1

Here's another one for releasing objects created by reflection:

We're working with a reporting tool (Active Reports 6) which creates a dll for each report.

We have lots of clients that use similar but still unique reports.

Reports are read through a web interface. We run multiple sites, one for each client.

Our choices are: 1) Put all the reports in one big project which will be called by all the sites. Cost: It will need to be recompiled every time we make a small change to any one report, potentially creating problems for all sites. 2) Create a whole bunch of similar little projects, with one for each site - let's say for sake of space that this creates problems, too. 3) Create a "Report Factory" which will use reflection to wire-up report dlls as needed.

We chose "3".

Problem: The final product works fine except for one thing: It won't release the report dll when done. There is not currently a problem with the operation within a test environment, but if you try to do anything in the folder with the report dlls, you get the following error message: "This action can't be completed because the folder or a file in it is open in another program"

After research on this site and others, we realized that we needed an AppDomain for each call which can be cleanly unloaded. After still having problems, we realized that the AppDomainSetup object needed to have a setting that allowed it to optimize for multiple users (LoaderOptimization.MultiDomain) That didn't work.

Unfortunately, the base object (Active 6 report) can not be serialized, so we can't make a deep copy and chuck the original object.

After doing all of this, we're still experiencing problems.

Here is the code (C#):

private object WireUpReport(ReportArgs args)
{

    //The parameter 'args' is a custom type (ReportArgs) which merely contains a 
name/value pair collection.


    object myReport = null;
    string sPath = String.Empty;
    string sFriendlyName = String.Empty;
    sFriendlyName = System.Guid.NewGuid().ToString();
    Assembly asmReport = null;
    AppDomainSetup ads = null;
    AppDomain adWireUp = null;
    ConstructorInfo ci = null;
    Type myReportType = null;
    Type[] parametypes = null;
    object[] paramarray = null;

    object retObject = null;

    try
    {

        //Get Report Object
        sPath = GetWireUpPath(args); //Gets the path to the required dll; kept in a config file
                                     //This parameter is used in an overloaded constructor further down

        ads = new AppDomainSetup();
        ads.ApplicationBase = Path.GetDirectoryName(sPath);
        ads.LoaderOptimization = LoaderOptimization.MultiDomain;
        adWireUp = AppDomain.CreateDomain(sFriendlyName, AppDomain.CurrentDomain.Evidence, ads);
        asmReport = adWireUp.GetAssemblies()[0];
        asmReport = Assembly.LoadFrom(sPath);

        //Create parameters for wireup
        myReportType = asmReport.GetExportedTypes()[0];
        parametypes = new Type[1];
        parametypes[0] = typeof(ReportArgs);
        ci = myReportType.GetConstructor(parametypes);
        paramarray = new object[1];
        paramarray[0] = args;

        //Instantiate object
        myReport = ci.Invoke(paramarray);

        return myReport;

    }
    catch (Exception ex)
    {
        throw ex;
    }
    finally
    {
        //Make sure Assembly object is released.
        if (adWireUp != null)
        {
            AppDomain.Unload(adWireUp);
        }
        if (asmReport != null)
        {
            asmReport = null;
        }

        if (ads != null)
        {
            ads = null;
        }
        if (adWireUp != null)
        {
            adWireUp = null;
        }
        if (ci != null)
        {
            ci = null;
        }
        if (myReportType != null)
        {
            myReportType = null;
        }
        if (parametypes != null)
        {
            parametypes = null;
        }
        if (paramarray != null)
        {
            paramarray = null;
        }

    }
}

The object which is returned from this code is cast as type ActiveReports and then passed around our application.

Any help would be deeply appreciated. Thanks

Chris Hannon
  • 4,134
  • 1
  • 21
  • 26

1 Answers1

2

Your code looks like you are seriously misunderstanding how to interact with a separate AppDomain.

Think of communicating with an AppDomain like talking to someone who's currently in another country. You know where they are, but you can't just walk over and talk to them. If you want them to do something for you, you have to open up a line of communication and tell them what you need.

The way that you open that line of communication is by defining a proxy object that can be created inside the other AppDomain and then cross the boundary back to your current AppDomain. Being able to cross the boundary requires that your object either be marked as [Serializable] or inherit from MarshalByRefObject. Because we actually want to talk to a reference in the other AppDomain and not just have a copy of it, we need the proxy to do the latter.

private class CrossDomainQuery : MarshalByRefObject
{
    public void LoadDataFromAssembly(string assemblyPath)
    {
        var assembly = Assembly.LoadFrom(assemblyPath);
        //TODO: Do something with your assembly
    }
}

There is a method on the AppDomain called CreateInstanceAndUnwrap() that will create an instance of that communication object inside the other AppDomain and then hand you back a __TransparentProxy object that can be cast to the proxy type.

var crossDomainQuery = (CrossDomainQuery)adWireUp.CreateInstanceAndUnwrap(
    typeof(CrossDomainQuery).Assembly.FullName,
    typeof(CrossDomainQuery).FullName);

Once you have that proxy object, you can call methods on it and they will be invoked in the other AppDomain.

crossDomainQuery.LoadDataFromAssembly(assemblyPath);

So how is this different from what your current example code is doing?

Your current code does not actually execute anything useful inside the other AppDomain.

adWireUp = AppDomain.CreateDomain(sFriendlyName, AppDomain.CurrentDomain.Evidence, ads);
asmReport = adWireUp.GetAssemblies()[0];
asmReport = Assembly.LoadFrom(sPath);

This creates a new AppDomain, but then it loads all of the assemblies from that AppDomain into your current AppDomain. Additionally, it explicitly loads your report assembly into your current AppDomain.

Creating an AppDomain and calling methods on it doesn't mean that your code is executing inside of it any more than reading about another country means that you're now talking to someone inside it.

Even if you do create a proxy object and execute code inside that other AppDomain, there are a few things to be aware of.

1) Both AppDomains must be able to see the type used for the proxy, and you may have to handle AssemblyResolve events for either AppDomain manually (at least temporarily) to help resolve that.

2) AppDomains are fairly expensive to create. Generally, they are not used in situations where you need to spin something up really quickly, take some action and disappear. You should plan on either keeping them around as long as you can or be prepared to take the performance hit on every call.

3) You've said that the report type that you're instantiating is not serializable, and being able to serialize the object is a requirement for passing that type back from the other AppDomain. Defining a serializable class that can transport relevant data across the boundary and using that to pass the report data might be an option, but you'll have to determine if that works for your particular situation.

Also, as an aside, unless you have logic that depends on variables being set to null, setting everything to null in your finally does nothing useful and complicates your code.

Chris Hannon
  • 4,134
  • 1
  • 21
  • 26
  • To Chris Hannon - Thanks very much for your thoughtful (and direct!) reply. I will try to apply what you've written, and then comment again. In the mean time, yes, I need to expand my knowledge in this area. Is there a good reference for the material you shared? Any good books or articles would be appreciated. The reason I ask involves the evolution of this code. We consulted a 'guru' site for the ConstructorInfo material. We then discovered the release problem (something the Guru failed to mention). More research pointed to AppDomain, but the material is spread all over the place. – Phil Brainerd Mar 07 '12 at 21:47
  • @PhilBrainerd As a start, I would suggest reading through the [MSDN documentation](http://msdn.microsoft.com/en-us/library/yb506139.aspx) and the answers to [this StackOverflow question.](http://stackoverflow.com/questions/622516/i-dont-understand-application-domains) – Chris Hannon Mar 09 '12 at 23:07
  • @PhilBrainerd Additionally, you may find [this MSDN Magazine article](http://msdn.microsoft.com/en-us/magazine/cc164072.aspx) on creating a plugin architecture to be helpful in understanding their overall context and use. – Chris Hannon Mar 09 '12 at 23:34
  • @PhilBrainerd Lastly, I'm glad you found my answer to be helpful. Since this is your first post, I'll also mention that, as with everything on StackOverflow, you are more than welcome to express that appreciation with an upvote or mark the answer as the official answer to your question. If you have additional questions, feel free to ask another question instead of commenting (it will more easily get more eyes than just mine on it). Welcome to StackOverflow! – Chris Hannon Mar 09 '12 at 23:42
  • Chris Hannon - Chris Thank you very much for the additional material. Just wanted to give an update for you and any others who may read this issue. The problem we've been having with the component has turned out to be a minor annoyance rather than a show-stopper, so I have the freedom to work on it in between other projects. The release problem seems to occur only in the context of IIS. End users don't see the problem, just tech people who try to make changes to the dlls involved. – Phil Brainerd Apr 06 '12 at 13:07