Loading an assembly and applying a predicate on its types in another appdomain
Asked Answered
N

2

8

Please read the entire question. I have a unique situation with several constraints that I'd like to solve.

In my code I have an expression tree which is compiled to a Predicate<System.Type>. My goal is to load an assembly without locking it (it is the output assembly of the project, being rebuilt constantly), apply this predicate on the list of its types and get back a list of the resulting type names:

// this is what I need:
return assembly.GetTypes().Where(t => predicate(t)).Select(t => t.FullName);

This assembly should be loaded in another appdomain, because I want to unload it as soon as I get the information I require.

Here is where it gets tricky. There are several problems I'm facing:

If I load the assembly in another appdomain and simply return an array of all of its types, so that I could apply the predicate back in my main appdomain, as soon as the Types are marshalled back to my main appdomain I get a FileNotFoundException, stating that the this assembly is not found. This makes sence, because the assembly is only being loaded in another appdomain I created. Loading it also in the main appdomain will defeat the purpose.

If, alternatively, I try to pass the predicate into the other appdomain, to apply it there and get back an array of strings (full type name), I get a SerializationException: "Cannot serialize delegates over unmanaged function pointers, dynamic methods or methods outside the delegate creator's assembly.", because the predicate is a Dynamic Method (compiled from an expression tree).

Loading it in the primary appdomain would have none of this problems, but since it's impossible to unload a loaded assembly without unloading the entire appdomain, as soon as the assembly would change (after rebuild), any attempt to load an assembly with the same name would result in an exception.

Context:
I am building a plugin for ReSharper called Agent Mulder. The idea behind the plugin is to analyze DI/IoC Container registrations in your solution, and help ReSharper figure out the usage of types registered via a DI Container (you can watch a short video of how it works here).

For the most part, analyzing container registration is straightforward - I only have to detect enough information to know which concrete types are affected. In this example with Castle Windsor: Component.For<IFoo>().ImplementedBy<Foo>() the resulting type is obvious, so is AllTypes.FromThisAssembly().BasedOn<IFoo>() - giving me just enough information to guestimate the concrete types that will be affected by this line. However, consider this registration in Castle Windsor:

container.Register(Classes
    .FromAssemblyInDirectory(new AssemblyFilter(".").FilterByName(an => an.Name.StartsWith("Ploeh.Samples.Booking")))
    .Where(t => !(t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Dispatcher<>)))
    .WithServiceAllInterfaces());

(source)

Here the information depends on a predicate that will only be evaluated at runtime.

Since all I can do is statically analyze this, I have in my hand the ReSharper's AST (called PSI in ReSharper) representation of the lambda expression from the Where clause. I can convert this AST into a LINQ expression tree, then compile it into a delegate.

My idea was is to load the output assembly (determined by the FromAssembly* descriptor) via reflection, and apply this delegate on the assembly's types in order to get the type names (I only need the names). This will have to be re-evaluated every time the assembly changes, too (I'm not concerned at this point about performance).

In conclusion, unless someone could recommend a better way of determine the types affected by the predicate, I'd like to know how to do this with reflection (unfortunately I hadn't considered other metadata readers, because I'd have to somehow convert the lambda expression AST to a predicate of different data type, and I don't know if there exists a 1-to-1 conversion).

Thank you for reading. This question will have a 500 point bounty when it becomes available.

Nicole answered 7/5, 2012 at 11:15 Comment(2)
If Expression was serializable, you could create the Predicate delegate as an Expression, and pass that over the appdomain boundary, and compile it in the 'remote' appdomain. But sadly, that is not that case :(Ratepayer
@Ratepayer Yes, I had thought about serializing the expression and passing it, you're right, it's not as simple as it sounds...Nicole
W
2

You need to load the assembly to get the Type instances, so a separate AppDomain seems like the right solution.

So, you need to get the predicate Expression into that AppDomain, which means you have to serialize / deserialize it.

This requirement is becoming more and more frequent for various reasons. I was looking at this because I wanted to squirt Linq to Entities expressions across a WCF service.

Fortunately, there are a few existing implementations.

I found this one: CodePlex - Expression Tree Serializer


I've just tested it with Types, and this works:

Expression<Func<Type,bool>> predicate =
  t => ( !t.IsGenericType && t.Name == "Int32" );

var s = new ExpressionSerialization.ExpressionSerializer();
var xml = s.Serialize( predicate );

var p = s.Deserialize( xml ) as Expression<Func<Type, bool>>;
var f = p.Compile();

Console.WriteLine( "Int32: " + f( typeof( int ) ) ); // true
Console.WriteLine( "String: " + f( typeof( string ) ) ); // false
Writeup answered 7/5, 2012 at 11:50 Comment(2)
Thank you! After additional acrobatics (XElement itself isn't marked [Serializable], and this breaks transfer between appdomains), I worked around it by converting to/from a string. For simple expressions this seems to do the job -- I will have to try with more complex ones. If this works, your answer might be it :)Nicole
You're welcome. That library saved me a lot of work. Please do let me know if you find any cases it doesn't handle.Writeup
D
1

Lets wrap predicate delegate instance with MBR object, parameter of type Type will be marshaled fine from other domain:

class RemotablePredicate<T> : MarshalByRefObject
{
  readonly Predicate<T> predicate;
  public RemotablePredicate(Predicate<T> predicate) { this.predicate = predicate; }
  public bool Accepts(T arg) { return predicate(arg); }
}

Build some type to load, explore assembly and return results to main domain:

class AssemblyExplorer : MarshalByRefObject
{
  public string[] GetTypesByPredicate(
    string assemblyPath, RemotablePredicate<Type> predicate)
  {
    // MS reflection api reqires all dependencies here
    var bytes = File.ReadAllBytes(assemblyPath);
    var assembly = Assembly.ReflectionOnlyLoad(bytes);

    var types = new List<string>();
    foreach (var type in assembly.GetTypes())
      if (predicate.Accepts(type))
        types.Add(type.FullName);

    return types.ToArray();
  }
}

Make all this work:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq.Expressions;
using System.Reflection;

class Program
{
  static void Main()
  {
    var fooDomain = AppDomain.CreateDomain("Foo");

    Expression<Predicate<Type>> expr = t => t.IsValueType;
    var compiledPredicate = expr.Compile();
    var remotablePredicate = new RemotablePredicate<Type>(compiledPredicate);

    var explorerType = typeof(AssemblyExplorer);
    var explorerInstance = (AssemblyExplorer) fooDomain
      .CreateInstanceAndUnwrap(explorerType.Assembly.FullName, explorerType.FullName);

    var types = explorerInstance.GetTypesByPredicate(
      "JetBrains.Annotations.dll", remotablePredicate);

    Console.WriteLine("Matched types: {0}", types.Length);
    foreach (var type in types) Console.WriteLine(" {0}", type);
  }
}
Dosage answered 7/5, 2012 at 13:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.