1

I have the following Class design:

public class UserDayStore
{
    public Guid Id { get; set; }
    public string UserName { get; set; }
    public string Password { get; set; }
    public DateTime EndOfSubscription { get; set; }
    public bool active { get; internal set; }
    public DateTime LastModified;

    public List<DbDayRecord>  Days { get; set; }
}

with

public class DbDayRecord
{
    public Guid StoreId { get; set; }
    public DateTime LastModified { get; set; }
    public DateTime DateOfDay { get; set; }

    public String Quote { get; set; }

}

Adding a UserDayStore works without a problem, also Adding a nested Item to List<Days> is ok. When I try to update a DayRecord, the Update is done as I can see in Robomongo. But when I try to search for a document after this in the UserStore collection I get this Exception:

System.FormatException

An error occurred while deserializing the Days property of class QuoteMyDayServer.ServerLogic.UserDayStore: Cannot deserialize a 'List' from BsonType 'Document'.

bei MongoDB.Driver.Linq.MongoQueryProviderImpl1.Execute(Expression expression) bei MongoDB.Driver.Linq.MongoQueryProviderImpl1.Execute[TResult](Expression expression) bei System.Linq.Queryable.First[TSource](IQueryable1 source) bei QuoteMyDayServer.ServerLogic.QuoteDatabase.<SetUserState>d__10.MoveNext() in C:\Entwicklung\Apps\QuoteMyDay\Server\QuoteMyDayServer\QuoteMyDayServer\ServerLogic\QuoteDatabase.cs:Zeile 147. --- Ende der Stapelüberwachung vom vorhergehenden Ort, an dem die Ausnahme ausgelöst wurde --- bei System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) bei System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) bei System.Runtime.CompilerServices.TaskAwaiter1.GetResult() bei NancyTests.MongoTests.d__8.MoveNext() in C:\Entwicklung\Apps\QuoteMyDay\Server\QuoteMyDayServer\NancyTests\MongoTests.cs:Zeile 148. --- Ende der Stapelüberwachung vom vorhergehenden Ort, an dem die Ausnahme ausgelöst wurde --- bei System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) bei System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) bei Xunit.Sdk.TestInvoker`1.<>c__DisplayClass46_1.<b__1>d.MoveNext() in C:\BuildAgent\work\cb37e9acf085d108\src\xunit.execution\Sdk\Frameworks\Runners\TestInvoker.cs:Zeile 227. --- Ende der Stapelüberwachung vom vorhergehenden Ort, an dem die Ausnahme ausgelöst wurde --- bei System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) bei System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) bei Xunit.Sdk.ExecutionTimer.d__4.MoveNext() in C:\BuildAgent\work\cb37e9acf085d108\src\xunit.execution\Sdk\Frameworks\ExecutionTimer.cs:Zeile 48. --- Ende der Stapelüberwachung vom vorhergehenden Ort, an dem die Ausnahme ausgelöst wurde --- bei System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) bei System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) bei Xunit.Sdk.ExceptionAggregator.d__9.MoveNext() in C:\BuildAgent\work\cb37e9acf085d108\src\xunit.core\Sdk\ExceptionAggregator.cs:Zeile 90.

System.FormatException

Cannot deserialize a 'List' from BsonType 'Document'.

bei MongoDB.Bson.Serialization.Serializers.EnumerableSerializerBase2.Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) bei MongoDB.Bson.Serialization.Serializers.SerializerBase1.MongoDB.Bson.Serialization.IBsonSerializer.Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) bei MongoDB.Bson.Serialization.IBsonSerializerExtensions.Deserialize(IBsonSerializer serializer, BsonDeserializationContext context) bei MongoDB.Bson.Serialization.BsonClassMapSerializer`1.DeserializeMemberValue(BsonDeserializationContext context, BsonMemberMap memberMap)

This is the method that Updates the DayRecord if one with the same "DateOfDay" exists, otherwise it will insert a new one.

    public async Task<bool> UpdateDayRecord(Guid storeID, DbDayRecord dayRecord)
    {
        var stores = GetUserStores();

        var builder = Builders<UserDayStore>.Filter;
        var filter = builder.Eq("Id", storeID);
        var result = await stores.FindAsync(filter);

        if (!await result.AnyAsync()) // Check is a Store with that Id exists
        {
            return false;
        }
        dayRecord.StoreId = storeID;

        filter =  builder.ElemMatch("Days", Builders<DbDayRecord>.Filter.Eq("DateOfDay", dayRecord.DateOfDay));
        result = await stores.FindAsync(filter);


        if (await result.AnyAsync())
        {

            var update = Builders<UserDayStore>.Update.Set("Days", dayRecord).CurrentDate("LastModified");

            var updateResult = await stores.UpdateOneAsync(filter, update);

            return (updateResult.ModifiedCount == 1);
        }
        else
        {

            filter = Builders<UserDayStore>.Filter.Eq("Id", storeID);
            var update = Builders<UserDayStore>.Update.AddToSet("Days", dayRecord).CurrentDate("LastModified");

            var updateResult = await stores.UpdateOneAsync(filter, update);

            return (updateResult.ModifiedCount == 1);
        }
    }

After the method is called and Updates an existing DayRecord I get the Exception above when trying access the UserDayStore:

    public async Task<Guid> GetStoreId (string username)
    { 
        var stores = GetUserStores();

        var filter = Builders<UserDayStore>.Filter.Eq("UserName", username);

        var result = await stores.FindAsync(filter);

        return result.First().Id;
    }

It fails in the FindAsync call.

This is what the JSON Document looks like after the update

{
    "_id" : LUUID("e858f1cc-c81d-7244-b8a0-8beec3c8e10d"),
    "LastModified" : ISODate("2016-04-23T10:43:17.293Z"),
    "UserName" : "TestUser",
    "Password" : "4242",
    "EndOfSubscription" : ISODate("2016-06-22T22:00:00.000Z"),
    "active" : true,
    "Days" : {
        "StoreId" : LUUID("e858f1cc-c81d-7244-b8a0-8beec3c8e10d"),
        "LastModified" : ISODate("2016-04-24T00:00:00.000Z"),
        "DateOfDay" : ISODate("2016-04-23T00:00:00.000Z"),
        "Quote" : "Testquote1"
    }
}
Thomas
  • 8,397
  • 7
  • 29
  • 39

5 Answers5

2

I believe your problem is that your Update statement uses Set in one case and AddToSet in another case. AddToSet is an array based operation, and Set assigns a value directly. You should use AddToSet in both circumstances in order to ensure an array exists in MongoDB.

The FormatException is because we are expecting an array (because the type is List) and instead we get a document.

Craig Wilson
  • 12,174
  • 3
  • 41
  • 45
  • I was not aware that AdToSet could be used in an Update Operation.Please, please improve the documentation. – Thomas Apr 23 '16 at 15:48
  • But why do you have to deserialize the document at all for doing a Find? Does that mean, that for any operating on the embedded documents the parent document and all embedded documents are loaded? Then perhaps I would be better of to put the List items in a separate collection. – Thomas Apr 23 '16 at 15:54
  • 1. In your code above, you are using AddToSet already in the else statement. Seems like you had found it already. 2. We only deserialize documents for a find that were found. The server sends back all the documents it found and we deserialize them all. 3. Schema design is a complex topic and whether to put things in their own collection or embedded really comes down to your use case. – Craig Wilson Apr 24 '16 at 01:02
  • For 1. The Else Statement should only be executed if there is not other Subdocument with the same date. Therefore it doesn't do an update but adds to the set. Still thinking naming a Funktion that Updates also AddToSet is not the most intuitive choice. 3. I will post a separate question on this. – Thomas Apr 24 '16 at 15:24
  • 4. Why is deserialization done on the Find and not when the first acess to the result is done via e.g. First? – Thomas Apr 24 '16 at 16:55
  • 1. We used the same name that MongoDB does (https://docs.mongodb.org/manual/reference/operator/update/). Whether it's a subdocument in an array that exists or not, you are still updating the stored document. 4. I think you might be misunderstanding something about MongoDB vs. a relational ORM. Embedded lists aren't lazily loaded. They aren't stored in a separate collection. They are part of the document. As such, when we get sent a document from the server, we deserialize all of it, not just part. – Craig Wilson Apr 24 '16 at 18:37
  • Thanks! So you get the documents from the server already with the Find method, not with the AccessMethods. This wasn't obvious. – Thomas Apr 24 '16 at 18:47
  • Hi Craig, it turns out that the problem was not that I use Set in one case and AddToSet in the other. Actually using AddToSet can not be used to update an existing array member. The solution was to use a .$ in the update definition. – Thomas Apr 27 '16 at 08:13
1

Well, I see couple of errors (most probably typo) so I give it a pass. However, your GetStoreId implementation is bit risky. What if there is no matching Store for a given username. You are assuming there will always be a document present which is wrong.

I have changed GetStoreId implementation little bit so swap it with yours and see if it work.

public async Task<Guid> GetStoreId (string username)
{       
    var cursor = await collection.FindAsync(x => x.UserName == username);

    var userDayStore = await cursor.FirstOrDefaultAsync();

    return userDayStore != null ? userDayStore.Id: Guid.Empty;
}
Saleem
  • 8,728
  • 2
  • 20
  • 34
1

After some try and error I think I know found the right way:

    filter =  builder.ElemMatch("Days", Builders<DbDayRecord>.Filter.Eq("DateOfDay", dayRecord.DateOfDay));
    result = await stores.FindAsync(filter);

    if (await result.AnyAsync())  // Record already exists, update it
    {

        var update = Builders<UserDayStore>.Update.Set("Days.$", dayRecord).CurrentDate("LastModified");

        var updateResult = await stores.UpdateOneAsync(filter, update);

        return (updateResult.ModifiedCount == 1);
    }
    else // Add new Record to array
    {

        filter = Builders<UserDayStore>.Filter.Eq("Id", storeID);
        var update = Builders<UserDayStore>.Update.AddToSet("Days", dayRecord).CurrentDate("LastModified");

        var updateResult = await stores.UpdateOneAsync(filter, update);

        return (updateResult.ModifiedCount == 1);
    }

The important point lies here:

    var update = Builders<UserDayStore>.Update.Set("Days.$", dayRecord).CurrentDate("LastModified");

Adding the .$ makes mongo updating the array element.

Thomas
  • 8,397
  • 7
  • 29
  • 39
0

Thanks to the help of @Saleem I found the problem.

Before the Update the JSON Documents looks like this:

{
    "_id" : LUUID("f7379cb0-bace-0442-9942-4452f8646522"),
    "LastModified" : ISODate("2016-04-23T12:59:31.358Z"),
    "UserName" : "TestUser",
    "Password" : "4242",
    "EndOfSubscription" : ISODate("2016-06-22T22:00:00.000Z"),
    "active" : true,
    "Days" : [ 
        {
            "StoreId" : LUUID("f7379cb0-bace-0442-9942-4452f8646522"),
            "LastModified" : ISODate("2016-04-24T00:00:00.000Z"),
            "DateOfDay" : ISODate("2016-04-23T00:00:00.000Z"),
            "Quote" : "Testquote1"
        }
    ]
}

So the problem lies in the Update. It changed the List to a DayRecord object.

As soon as I have a working solution I will update this question

Thomas
  • 8,397
  • 7
  • 29
  • 39
0

Instead of

var update = Builders<UserDayStore>.Update.Set("Days.$", dayRecord).CurrentDate("LastModified");

you can also do it like this:

var update = Builders<UserDayStore>.Update.Set(x => x.Days[-1], dayRecord).CurrentDate("LastModified");

Source: http://www.mattburkedev.com/updating-inside-a-nested-array-with-the-mongodb-positional-operator-in-c-number/