MarshalByRefObject with iterator method (IEnumerable<T>) not working

1

I'm trying to make an application that can dynamically load and unload an after-market assembly and create an object of a type defined in that assembly, but I'm running into problems when the object has an iterator method.

Stay with me -- the Minimal, Reproducible Example is a little bit big because it has multiple parts. I'll explain it in three phases.

Phase 1

Here's the basic structure without the plugin architecture in place. This is all glommed into one assembly here only to illustrate the structure I'm going for.

using API;

namespace API
{
    public interface IHostObject
    {
        string Name { get; set; }
    }

    public interface IPluginObject
    {
        void DoSomething(API.IHostObject hostObject);
    }
}


namespace Plugin
{
    class ConcretePluginObject : API.IPluginObject
    {
        void IPluginObject.DoSomething(IHostObject hostObject)
        {
            System.Console.WriteLine(hostObject.Name);
        }
    }
}

namespace Host
{
    class ConcreteHostObject : API.IHostObject
    {
        public string Name { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            IHostObject hostObject = new ConcreteHostObject() { Name = "Hosty McHostface" };
            IPluginObject pluginObject = new Plugin.ConcretePluginObject();

            pluginObject.DoSomething(hostObject);
        }
    }
}

Phase 2

Then I split this project into three pieces to make the plugin architecture.

API.dll

  • API.IHostObject
  • API.IPluginObject

Host.exe

  • main
  • ConcreteHostObject

Plugin.dll

  • ConcretePluginObject

I have some activation code that does this:

using System;
using API;

namespace Host
{
    class ConcreteHostObject : MarshalByRefObject, API.IHostObject
    {
        public string Name { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var appDir = AppDomain.CurrentDomain.BaseDirectory;
            var pluginsDir = System.IO.Path.Combine(appDir, "Plugins");
            var appDomainSetup = new AppDomainSetup {
                ApplicationName = "",
                ShadowCopyDirectories = "true",
                ApplicationBase = pluginsDir,
                CachePath = "VSSCache"
            };
            AppDomain apd = AppDomain.CreateDomain("NewZealand", null, appDomainSetup);

            API.IPluginObject pluginObject = (API.IPluginObject)apd.CreateInstance("Plugin", "Plugin.ConcretePluginObject").Unwrap();

            IHostObject hostObject = new ConcreteHostObject() { Name = "Hosty McHostface" };
            pluginObject.DoSomething(hostObject);
        }
    }
}

This all works great so far.

Phase 3 - Here's the problem

I was under the impression that as long as I only access the objects via interfaces that are defined in a common API assembly that everything would be fine. But now I'm getting into trouble when I add an IEnumerable<string> function into my IPluginObject.

    public interface IPluginObject
    {
        void DoSomething(API.IHostObject hostObject);
        IEnumerable<string> GetStrings(); // Added this
    }

And it's implemented like this:

using System;
using System.Collections.Generic;
using API;

namespace Plugin
{
    class ConcretePluginObject : MarshalByRefObject, API.IPluginObject
    {
        void IPluginObject.DoSomething(IHostObject hostObject)
        {
            System.Console.WriteLine(hostObject.Name);
        }

        public IEnumerable<string> GetStrings() // Added this iterator method
        {
            yield return "one";
            yield return "two";
            yield return "three";
        }
    }
}

Now when I call pluginObject.GetStrings(), I get an exception:

System.Runtime.Serialization.SerializationException
  HResult=0x8013150C
  Message=Type 'Plugin.ConcretePluginObject+<GetStrings>d__1' in Assembly 'Plugin, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' is not marked as serializable.
  Source=mscorlib
  StackTrace:
   at System.Runtime.Remoting.Proxies.RealProxy.HandleReturnMessage(IMessage reqMsg, IMessage retMsg)
   at System.Runtime.Remoting.Proxies.RealProxy.PrivateInvoke(MessageData& msgData, Int32 type)
   at API.IPluginObject.GetStrings()
   at Host.Program.Main(String[] args) in E:\Dev\Test\PluginTest\Host\Program.cs:line 27

I thought this was going to work, but there seems to be something about the iterator (the function that returns IEnumerable and makes use of the yield keyword to do so) that makes it stop working.

What's going on here?

I admit I can't make sense of the d__1 suffix in the type name Plugin.ConcretePluginObject+<GetStrings>d__1 but I'm thinking it has something to do with the iterator method. I also had a look through these docs, especially the part about the requirements for iterator methods, but it doesn't say anything about serialization requirements.

Can someone please explain what went wrong and what I can do to fix it?

Important requirement

This is a Minimal, Reproducible Example. But in my actual plugin, the GetStrings method is actually an iterator method that works like a coroutine, meaning it is not an acceptable workaround to switch from using IEnumerable<string> to using string[]. There is no collection of strings, and there is no array. This is really an honest-to-goodness iterator method that makes use of yield and works like a coroutine.

c#
iterator
yield
appdomain
marshalbyrefobject
asked on Stack Overflow Mar 2, 2020 by Wyck

1 Answer

1

The problem is that every type that crosses the AppDomain boundary must be serializable. You may notice that MarshalByRefObject is marked with the [Serializable] attribute, which is why your ConcreteHostObject was able to cross over just fine.

An iterator method, however, does some compiler magic under the covers to allow it to work correctly and creates (and returns) a class it defined that implements IEnumerable<T>. The d__1 suffix is a good clue that this isn't a class of your own construction. Unfortunately, this class is not marked as serializable. If you want that behavior, you'll have to write it yourself and manage your own 'yield' logic.

answered on Stack Overflow Mar 2, 2020 by Chris Hannon

User contributions licensed under CC BY-SA 3.0