Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MongoDB C# driver - Change Id serialization for inherited class

I have implemented Repository pattern with a base entity class for my collections. Till now all collections had _id of ObjectId type. In the code, I needed to represent the Id as a string.

Here is how the EntityBase class look like

public abstract class EntityBase
{
    [BsonRepresentation(BsonType.ObjectId)]
    public virtual string Id { get; set; }
}

Here is the mapping:

BsonClassMap.RegisterClassMap<EntityBase>(cm =>
{
    cm.AutoMap();
    cm.MapIdProperty(x => x.Id).SetIdGenerator(StringObjectIdGenerator.Instance);
    cm.IdMemberMap.SetSerializer(new StringSerializer().WithRepresentation(BsonType.ObjectId));
});

Now I have a Language collection which Id will be plain string something like en-GB.

{
   "_id" : "en-GB",
   "Term1" : "Translation 1",
   "Term2" : "Translation 2"
}

Language class is inheriting the EntityBase class

public class Language : EntityBase
{
    [BsonExtraElements]
    public IDictionary<string, object> Terms { get; set; }
    public override string Id { get; set; }
}

The question is can I somehow change how the Id is serialized only for the Language class?

I don't want to change the behaviour of EntityBase class since I have a lot of other collections inheriting the EntityBase.

Update

Here is what I tried and got exception. Not sure if what I have tried is possible.

BsonClassMap.RegisterClassMap<Language>(cm =>
{
    cm.AutoMap();
    cm.MapExtraElementsMember(c => c.Terms);
    cm.MapIdProperty(x => x.Id).SetIdGenerator(StringObjectIdGenerator.Instance);
    cm.IdMemberMap.SetSerializer(new StringSerializer().WithRepresentation(BsonType.String));
});

Here is the exception that I was getting:

An exception of type 'System.ArgumentOutOfRangeException' occurred in MongoDB.Bson.dll but was not handled in user code

Additional information: The memberInfo argument must be for class Language, but was for class EntityBase.

at MongoDB.Bson.Serialization.BsonClassMap.EnsureMemberInfoIsForThisClass(MemberInfo memberInfo)
at MongoDB.Bson.Serialization.BsonClassMap.MapMember(MemberInfo memberInfo)
at MongoDB.Bson.Serialization.BsonClassMap`1.MapMember[TMember](Expression`1 memberLambda)
at MongoDB.Bson.Serialization.BsonClassMap`1.MapProperty[TMember](Expression`1 propertyLambda)
at MongoDB.Bson.Serialization.BsonClassMap`1.MapIdProperty[TMember](Expression`1 propertyLambda)
at Test.Utilities.MongoDbClassConfig.<>c.<Configure>b__0_1(BsonClassMap`1 cm) in F:\Development\Test\Utilities\MongoDbClassConfig.cs:line 23
at MongoDB.Bson.Serialization.BsonClassMap`1..ctor(Action`1 classMapInitializer)
at MongoDB.Bson.Serialization.BsonClassMap.RegisterClassMap[TClass](Action`1 classMapInitializer)
at Test.Utilities.MongoDbClassConfig.Configure() in F:\Development\Test\Utilities\MongoDbClassConfig.cs:line 20
at Test.Portal.BackEnd.Startup..ctor(IHostingEnvironment env) in F:\Development\Test\Startup.cs:line 43
like image 271
zoranpro Avatar asked Jul 20 '15 21:07

zoranpro


People also ask

Does MongoDB use C++?

Welcome to the documentation site for the official MongoDB C++ driver. You can add the driver to your application to work with MongoDB using the C++11 or later standard.

Can you use MongoDB with C#?

By developing with C# and MongoDB together one opens up a world of possibilities. Console, window, and web applications are all possible. As are cross-platform mobile applications using the Xamarin framework.

What is a MongoDB driver?

The official MongoDB Node. js driver allows Node. js applications to connect to MongoDB and work with data. The driver features an asynchronous API which allows you to interact with MongoDB using Promises or via traditional callbacks.


3 Answers

I may be late with an answer, but I had the same problem, found this question, but no real answer how to solve and why is it happening.

What is happening is that Mongo driver doesn't explicitly understand polymorphism, and you must manually mention every type. All the types that inherit from the same class must be listed and explained to BsonClassMap.

In your case when you define your EntityBase you can list all the children that will be serialized and it would get it working. So something along the lines of:

[BsonDiscriminator(RootClass = true)]
[BsonKnownTypes(typeof(Language), typeof(OtherEntity))]
public abstract class EntityBase
{
    [BsonId]
    public virtual string Id { get; set; }
}

should work. In my case, I wanted to have a generic Entity save method and my entities are not aware of the Mongo driver so I could not use attributes. I had my ID in the EntityBase, so here's a simple generic add method (just the relevant, but working part):

public async Task Add<T>(T item) where T : EntityBase, new()
{
    BsonClassMap.RegisterClassMap<EntityBase>(cm =>
    {
        cm.AutoMap();
        cm.MapIdMember(p => p.Id);
        cm.SetIsRootClass(true);
    });
    BsonClassMap.RegisterClassMap<T>();

    await Db.GetCollection<T>(typeof(T).Name).InsertOneAsync(item);
}

As you can see, I must map the base class with every entry. Then the object is properly serialized and saved in the database.

like image 60
Fedor Hajdu Avatar answered Nov 10 '22 05:11

Fedor Hajdu


When you use the attribute [BsonRepresentation(BsonType.ObjectId)] you are telling the serializer, that even though this is a string — you want it represented by the ObjectId type. "en-GB" is not a valid ObjectId therefor, the serializer throws an exception.

Use the [BsonId]-attribute if you want to specify that your property named Id must be a unique identifier within your collection.

There is also an ObjectId-data type within the driver, which you can implement instead.

I would add a generic type-parameter to your EntityBase-class.

public abstract class EntityBase<TId>
{
    [BsonId]
    public TId Id { get; set; }
}

You could also implement a EntityBase-class, without the generic type argument as such:

public abstract class EntityBase : EntityBase<ObjectId>
{
}

Then for your Language-class

public class Language : EntityBase<string>
{
    [BsonExtraElements]
    public IDictionary<string, object> Terms { get; set; }
}

The driver should not have any trouble serializing a string-representation of an ObjectId to an ObjectId; but i would try it first — just to make sure.

like image 5
mausworks Avatar answered Nov 10 '22 06:11

mausworks


I have just spent a few hours on this problem and was stumped - but I now have the Solution!! The problem with Gurgen's answer is that SuperBaseEntity.Id is always null - since the Id Property is overriden. I have managed this:

public abstract class Base
{
  [BsonIgnore]
  public abstract string Id {get;set}
}
public class ClassWithStringId : Base
{
  [BsonElement("_id")]
  [BsonId]
  public override string Id {get;set;}
}
public classs ClassWIthOidId : Base
{
  [BsonElement("_id")]
  [BsonId]
  public override string Id {get;set;}
}
...
//Class Map where the magic happens
BsonClassMap.RegisterClassMap<Base>(o =>
{
  o.SetIsRootClass(true);
  o.AutoMap();
}
BsonClassMap.RegisterClassMap<ClassWithOidId>(o =>
{
  o.AutoMap();
  //o.SetDiscriminator("...")//you probably need this
  o.MapIdProperty("Id"); //!!!! This is the solution! You need to reference the Id Property via string instead of the normal Expression c => c.Id, the latter will lead to an exception: "MemberInfo was for Type Base but not for Type XYZ"
  o.IdMemberMap.SetSerializer(new StringSerializer().WithRepresentation(BsonType.YourFavoriteType));
}

like image 3
monomo Avatar answered Nov 10 '22 04:11

monomo