Ambiguous discriminator 'myType' when '

2019-07-18 15:49发布

问题:

I have an application which creates new types dynamically at run time, creates objects of that type and inserts them into a MongoDB database collection of type object. Using the shell I can see that the object is inserted correctly and the _t value is the correct name of the dynamically created class.

I am trying to retrieve objects from my collection using AsQueryable while applying a LINQ query to filter the results to only objects of a specific type.

This works fine:

_collection.AsQueryable<object>();

while this:

_collection.AsQueryable<object>().Where(t => t.GetType() == type);

throws the exception:

Ambiguous discriminator 'myType'
   at MongoDB.Bson.Serialization.BsonSerializer.LookupActualType(Type nominalType, BsonValue discriminator)
   at MongoDB.Bson.Serialization.Conventions.StandardDiscriminatorConvention.GetActualType(BsonReader bsonReader, Type nominalType)
   at MongoDB.Bson.Serialization.Serializers.ObjectSerializer.Deserialize(BsonReader bsonReader, Type nominalType, IBsonSerializationOptions options)
   at MongoDB.Driver.Internal.MongoReplyMessage`1.ReadBodyFrom(BsonBuffer buffer)
   at MongoDB.Driver.Internal.MongoReplyMessage`1.ReadFrom(BsonBuffer buffer)
   at MongoDB.Driver.Internal.MongoConnection.ReceiveMessage[TDocument](BsonBinaryReaderSettings readerSettings, IBsonSerializer serializer, IBsonSerializationOptions serializationOptions)
   at MongoDB.Driver.Operations.QueryOperation`1.GetFirstBatch(IConnectionProvider connectionProvider)
   at MongoDB.Driver.Operations.QueryOperation`1.Execute(IConnectionProvider connectionProvider)
   at MongoDB.Driver.MongoCursor`1.GetEnumerator()
   at MongoDB.Driver.Linq.IdentityProjector`1.GetEnumerator()
   at System.Linq.Enumerable.First[TSource](IEnumerable`1 source)
   at MongoDB.Driver.Linq.SelectQuery.<TranslateFirstOrSingle>b__a(IEnumerable source)
   at MongoDB.Driver.Linq.SelectQuery.Execute()
   at MongoDB.Driver.Linq.MongoQueryProvider.Execute(Expression expression)
   at MongoDB.Driver.Linq.MongoQueryProvider.Execute[TResult](Expression expression)
   at System.Linq.Queryable.First[TSource](IQueryable`1 source)
   at MongoDBTest.Program.RetreiveTransaction(String transactionType, Int32 version) in c:\projects\mrp\trunk\Source\POC\MongoDBTest\MongoDBTest\Program.cs:line 192
   at MongoDBTest.Program.DynamicDBExample() in c:\projects\mrp\trunk\Source\POC\MongoDBTest\MongoDBTest\Program.cs:line 163
   at MongoDBTest.Program.Main(String[] args) in c:\projects\mrp\trunk\Source\POC\MongoDBTest\MongoDBTest\Program.cs:line 28
   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.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.ThreadHelper.ThreadStart()

when the type is my dynamically generated type (it works fine for other types).

Also this does work:

_collection.FindAs<object>(Query.EQ("_t", type.Name)).AsQueryable();

but unfortunately it returns all the documents of that type from the database and then any LINQ queries are performed locally instead of at the database, which is not what I want.


Here is the code I am using to create types at run time:

public static Type CompileResultType(string className, Dictionary<string, string> fields)
{
    TypeBuilder tb = GetTypeBuilder(className);
    ConstructorBuilder constructor = tb.DefineDefaultConstructor(MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName);

    foreach (var field in fields)
    {
        CreateProperty(tb, field.Key, Type.GetType(field.Value));
    }

    Type objectType = tb.CreateType();
    return objectType;
}

private static TypeBuilder GetTypeBuilder(string className)
{
    var an = new AssemblyName("DynamicClassAssembly");
    AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(an, AssemblyBuilderAccess.Run);
    ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule");
    TypeBuilder tb = moduleBuilder.DefineType("DynamicClassNamespace."+className
                        , TypeAttributes.Public |
                        TypeAttributes.Class |
                        TypeAttributes.AutoClass |
                        TypeAttributes.AnsiClass |
                        TypeAttributes.BeforeFieldInit |
                        TypeAttributes.AutoLayout
                        , null);
    return tb;
}

private static void CreateProperty(TypeBuilder tb, string propertyName, Type propertyType)
{
    FieldBuilder fieldBuilder = tb.DefineField("_" + propertyName, propertyType, FieldAttributes.Private);

    PropertyBuilder propertyBuilder = tb.DefineProperty(propertyName, PropertyAttributes.HasDefault, propertyType, null);
    MethodBuilder getPropMthdBldr = tb.DefineMethod("get_" + propertyName, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, propertyType, Type.EmptyTypes);
    ILGenerator getIl = getPropMthdBldr.GetILGenerator();

    getIl.Emit(OpCodes.Ldarg_0);
    getIl.Emit(OpCodes.Ldfld, fieldBuilder);
    getIl.Emit(OpCodes.Ret);

    MethodBuilder setPropMthdBldr =
        tb.DefineMethod("set_" + propertyName,
            MethodAttributes.Public |
            MethodAttributes.SpecialName |
            MethodAttributes.HideBySig,
            null, new[] { propertyType });

    ILGenerator setIl = setPropMthdBldr.GetILGenerator();
    Label modifyProperty = setIl.DefineLabel();
    Label exitSet = setIl.DefineLabel();

    setIl.MarkLabel(modifyProperty);
    setIl.Emit(OpCodes.Ldarg_0);
    setIl.Emit(OpCodes.Ldarg_1);
    setIl.Emit(OpCodes.Stfld, fieldBuilder);

    setIl.Emit(OpCodes.Nop);
    setIl.MarkLabel(exitSet);
    setIl.Emit(OpCodes.Ret);

    propertyBuilder.SetGetMethod(getPropMthdBldr);
    propertyBuilder.SetSetMethod(setPropMthdBldr);
}

Full code that exhibits this behavior:

static void ProcessTransaction(IncomingTransaction input, string transactionType, int version)
{
    var configuration = GetConfiguration(transactionType, version);

    //configuration.Fields is just a Dictionary of field names -> types
    Type dynamicType = DynamicClassHelper.CompileResultType(transactionType + version, configuration.Fields);

    object transaction = Activator.CreateInstance(dynamicType);

    //AutoMapper, just populates the data on transaction object
    Mapper.DynamicMap(input, transaction, typeof(IncomingTransaction), transaction.GetType());

    //Just a wrapper around MongoDB, creates a MongoCollection<object>
    var db = new MongoTransactionDB<object>(connectionString, databaseName, "transactions", new ConsoleLogger());

    //just calls Insert() on the collection
    db.AddTransaction(transaction);
}

static void RetreiveTransaction(string transactionType, int version)
{
    var db = new MongoTransactionDB<object>(connectionString, databaseName, "transactions", new ConsoleLogger());

    var config = GetConfiguration(transactionType, version);

    Type dynamicType = DynamicClassHelper.CompileResultType(transactionType + version, config.Fields);

    //!!! This is where the exception is thrown !!!
    var result = db.GetAllTransactionsOfType(dynamicType).First();
}

//From MongoTransactionDB class...
public IQueryable<TTransactionBase> GetAllTransactionsOfType(Type type)
{
    return _collection.AsQueryable().Where(t => t.GetType() == type);
}

Result after inserting the dynamic object into MongoDB (transactionType = "Cash" and version = 1):

回答1:

You can always combine mongo queries with LINQ using Inject and have them run in MongoDB and not the client:

var queryable = _collection.AsQueryable().Where(_ => Query.EQ("_t", type.Name).Inject()).Where(...);

Inject is a pseudo-method that is used to inject a lower level MongoDB query into a LINQ query.



回答2:

From some experimentation I was able to find the cause of this error.

If I create the dynamic type just once and use that same Type object for inserting and retrieving then it works fine. Also even if I create the Type object every time I insert or retrieve it works fine as long as the insert and retrieval occur during different executions of the application.

This indicates that the problem is related to how MongoDB automatically creates class registrations. I think what is going on is that when inserting the object MongoDB creates a class map for that instance of the Type object, and then when retrieving MongoDB is creating another class map automatically using the new Type instance and there is ambiguity because now MongoDB has 2 different class maps registered which both have the same name.

I was able to fix this by using a cache of Type objects so that there will only by 1 instance of each dynamically created type during run time:

static Type GetTransactionType(string transactionType, int version)
{
    string key = transactionType + version;

    if (!typeCache.ContainsKey(key))
    {
        var configuration = GetConfiguration(transactionType, version);

        Type dynamicType = DynamicClassHelper.CompileResultType(transactionType + version, configuration.Fields);

        typeCache.Add(key, dynamicType);
    }

    return typeCache[key];
}