From 68f8ef8fb529bb06aa3e9c29dfa877ebc3ccf25a Mon Sep 17 00:00:00 2001 From: rstam Date: Wed, 13 Jul 2011 14:45:04 -0400 Subject: [PATCH] Fixed CSHARP-265. DictionarySerializer and DictionaryGenericSerializer now switch to the alternate array representation when any key value is not a valid element name. The client code can also force use of Array or Document representation using serialization options (perhaps for efficiency reasons to skip scanning the keys for validity as element names). --- .../DictionaryGenericSerializer.cs | 54 +++- .../Serializers/DictionarySerializer.cs | 49 +++- DriverOnlineTests/DriverOnlineTests.csproj | 1 + DriverOnlineTests/Jira/CSharp265Tests.cs | 269 ++++++++++++++++++ 4 files changed, 345 insertions(+), 28 deletions(-) create mode 100644 DriverOnlineTests/Jira/CSharp265Tests.cs diff --git a/Bson/Serialization/Serializers/DictionaryGenericSerializer.cs b/Bson/Serialization/Serializers/DictionaryGenericSerializer.cs index 057406fe74..4ceb469c5d 100644 --- a/Bson/Serialization/Serializers/DictionaryGenericSerializer.cs +++ b/Bson/Serialization/Serializers/DictionaryGenericSerializer.cs @@ -21,6 +21,7 @@ using System.IO; using MongoDB.Bson.IO; using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Options; namespace MongoDB.Bson.Serialization.Serializers { /// @@ -108,25 +109,48 @@ namespace MongoDB.Bson.Serialization.Serializers { bsonWriter.WriteNull(); } else { var dictionary = (IDictionary) value; - if ( - typeof(TKey) == typeof(string) || - (typeof(TKey) == typeof(object) && dictionary.Keys.All(o => o.GetType() == typeof(string))) - ) { - bsonWriter.WriteStartDocument(); - foreach (KeyValuePair entry in dictionary) { - bsonWriter.WriteName((string) (object) entry.Key); - BsonSerializer.Serialize(bsonWriter, typeof(TValue), entry.Value); + + var representationOptions = options as RepresentationSerializationOptions; + BsonType representation; + if (representationOptions == null) { + if (typeof(TKey) == typeof(string) || typeof(TKey) == typeof(object)) { + representation = BsonType.Document; + foreach (object key in dictionary.Keys) { + var name = key as string; // check for null and type string at the same time + if (name == null || name.StartsWith("$") || name.Contains(".")) { + representation = BsonType.Array; + break; + } + } + } else { + representation = BsonType.Array; } - bsonWriter.WriteEndDocument(); } else { - bsonWriter.WriteStartArray(); - foreach (KeyValuePair entry in dictionary) { + representation = representationOptions.Representation; + } + + switch (representation) { + case BsonType.Document: + bsonWriter.WriteStartDocument(); + foreach (KeyValuePair entry in dictionary) { + bsonWriter.WriteName((string) (object) entry.Key); + BsonSerializer.Serialize(bsonWriter, typeof(TValue), entry.Value); + } + bsonWriter.WriteEndDocument(); + break; + case BsonType.Array: bsonWriter.WriteStartArray(); - BsonSerializer.Serialize(bsonWriter, typeof(TKey), entry.Key); - BsonSerializer.Serialize(bsonWriter, typeof(TValue), entry.Value); + foreach (KeyValuePair entry in dictionary) { + bsonWriter.WriteStartArray(); + BsonSerializer.Serialize(bsonWriter, typeof(TKey), entry.Key); + BsonSerializer.Serialize(bsonWriter, typeof(TValue), entry.Value); + bsonWriter.WriteEndArray(); + } bsonWriter.WriteEndArray(); - } - bsonWriter.WriteEndArray(); + break; + default: + var message = string.Format("'{0}' is not a valid representation for type IDictionary<{1}, {2}>.", representation, typeof(TKey).Name, typeof(TValue).Name); + throw new BsonSerializationException(message); } } } diff --git a/Bson/Serialization/Serializers/DictionarySerializer.cs b/Bson/Serialization/Serializers/DictionarySerializer.cs index 60b822e1dd..0519b41328 100644 --- a/Bson/Serialization/Serializers/DictionarySerializer.cs +++ b/Bson/Serialization/Serializers/DictionarySerializer.cs @@ -22,6 +22,7 @@ using System.IO; using MongoDB.Bson.IO; using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Options; namespace MongoDB.Bson.Serialization.Serializers { /// @@ -121,23 +122,45 @@ namespace MongoDB.Bson.Serialization.Serializers { bsonWriter.WriteNull(); } else { var dictionary = (IDictionary) value; - if (dictionary.Keys.Cast().All(o => o.GetType() == typeof(string))) { - bsonWriter.WriteStartDocument(); - foreach (DictionaryEntry entry in dictionary) { - bsonWriter.WriteName((string) entry.Key); - BsonSerializer.Serialize(bsonWriter, typeof(object), entry.Value); + + var representationOptions = options as RepresentationSerializationOptions; + BsonType representation; + if (representationOptions == null) { + representation = BsonType.Document; + foreach (object key in dictionary.Keys) { + var name = key as string; // check for null and type string at the same time + if (name == null || name.StartsWith("$") || name.Contains(".")) { + representation = BsonType.Array; + break; + } } - bsonWriter.WriteEndDocument(); } else { - bsonWriter.WriteStartArray(); - foreach (DictionaryEntry entry in dictionary) { + representation = representationOptions.Representation; + } + + switch (representation) { + case BsonType.Document: + bsonWriter.WriteStartDocument(); + foreach (DictionaryEntry entry in dictionary) { + bsonWriter.WriteName((string) entry.Key); + BsonSerializer.Serialize(bsonWriter, typeof(object), entry.Value); + } + bsonWriter.WriteEndDocument(); + break; + case BsonType.Array: bsonWriter.WriteStartArray(); - BsonSerializer.Serialize(bsonWriter, typeof(object), entry.Key); - BsonSerializer.Serialize(bsonWriter, typeof(object), entry.Value); + foreach (DictionaryEntry entry in dictionary) { + bsonWriter.WriteStartArray(); + BsonSerializer.Serialize(bsonWriter, typeof(object), entry.Key); + BsonSerializer.Serialize(bsonWriter, typeof(object), entry.Value); + bsonWriter.WriteEndArray(); + } bsonWriter.WriteEndArray(); - } - bsonWriter.WriteEndArray(); - } + break; + default: + var message = string.Format("'{0}' is not a valid representation for type IDictionary.", representation); + throw new BsonSerializationException(message); + } } } #endregion diff --git a/DriverOnlineTests/DriverOnlineTests.csproj b/DriverOnlineTests/DriverOnlineTests.csproj index b4431f6f09..33f87c7569 100644 --- a/DriverOnlineTests/DriverOnlineTests.csproj +++ b/DriverOnlineTests/DriverOnlineTests.csproj @@ -105,6 +105,7 @@ + diff --git a/DriverOnlineTests/Jira/CSharp265Tests.cs b/DriverOnlineTests/Jira/CSharp265Tests.cs new file mode 100644 index 0000000000..5db29c44f0 --- /dev/null +++ b/DriverOnlineTests/Jira/CSharp265Tests.cs @@ -0,0 +1,269 @@ +/* Copyright 2010-2011 10gen Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.Serialization; +using System.Text; +using NUnit.Framework; + +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Driver; +using MongoDB.Driver.Builders; + +namespace MongoDB.DriverOnlineTests.Jira.CSharp265 { + [TestFixture] + public class CSharp265Tests { + public class GDA { + public int Id; + [BsonRepresentation(BsonType.Array)] + public Dictionary Data; + } + + public class GDD { + public int Id; + [BsonRepresentation(BsonType.Document)] + public Dictionary Data; + } + + public class GDX { + public int Id; + public Dictionary Data; + } + + public class HA { + public int Id; + [BsonRepresentation(BsonType.Array)] + public Hashtable Data; + } + + public class HD { + public int Id; + [BsonRepresentation(BsonType.Document)] + public Hashtable Data; + } + + public class HX { + public int Id; + public Hashtable Data; + } + + private MongoServer server; + private MongoDatabase database; + private MongoCollection collection; + + [TestFixtureSetUp] + public void TestFixtureSetup() { + server = MongoServer.Create("mongodb://localhost/?safe=true"); + database = server["onlinetests"]; + collection = database.GetCollection("testcollection"); + collection.Drop(); + } + + [Test] + public void TestGenericDictionaryArrayRepresentationWithDollar() { + var d = new GDA { Id = 1, Data = new Dictionary { { "$a", 1 } } }; + var expected = "{ '_id' : 1, 'Data' : [['$a', 1]] }".Replace("'", "\""); + var json = d.ToJson(); + Assert.AreEqual(expected, json); + + collection.RemoveAll(); + collection.Insert(d); + var r = collection.FindOne(Query.EQ("_id", d.Id)); + Assert.AreEqual(d.Id, r.Id); + Assert.AreEqual(1, r.Data.Count); + Assert.AreEqual(1, r.Data["$a"]); + } + + [Test] + public void TestGenericDictionaryArrayRepresentationWithDot() { + var d = new GDA { Id = 1, Data = new Dictionary { { "a.b", 1 } } }; + var expected = "{ '_id' : 1, 'Data' : [['a.b', 1]] }".Replace("'", "\""); + var json = d.ToJson(); + Assert.AreEqual(expected, json); + + collection.RemoveAll(); + collection.Insert(d); + var r = collection.FindOne(Query.EQ("_id", d.Id)); + Assert.AreEqual(d.Id, r.Id); + Assert.AreEqual(1, r.Data.Count); + Assert.AreEqual(1, r.Data["a.b"]); + } + + [Test] + public void TestGenericDictionaryDocumentRepresentationWithDollar() { + var d = new GDD { Id = 1, Data = new Dictionary { { "$a", 1 } } }; + var expected = "{ '_id' : 1, 'Data' : { '$a' : 1 } }".Replace("'", "\""); + var json = d.ToJson(); + Assert.AreEqual(expected, json); + + Assert.Throws(() => { collection.Insert(d); }); + } + + [Test] + public void TestGenericDictionaryDocumentRepresentationWithDot() { + var d = new GDD { Id = 1, Data = new Dictionary { { "a.b", 1 } } }; + var expected = "{ '_id' : 1, 'Data' : { 'a.b' : 1 } }".Replace("'", "\""); + var json = d.ToJson(); + Assert.AreEqual(expected, json); + + Assert.Throws(() => { collection.Insert(d); }); + } + + [Test] + public void TestGenericDictionaryDynamicRepresentationNormal() { + var d = new GDX { Id = 1, Data = new Dictionary { { "abc", 1 } } }; + var expected = "{ '_id' : 1, 'Data' : { 'abc' : 1 } }".Replace("'", "\""); + var json = d.ToJson(); + Assert.AreEqual(expected, json); + + collection.RemoveAll(); + collection.Insert(d); + var r = collection.FindOne(Query.EQ("_id", d.Id)); + Assert.AreEqual(d.Id, r.Id); + Assert.AreEqual(1, r.Data.Count); + Assert.AreEqual(1, r.Data["abc"]); + } + + [Test] + public void TestGenericDictionaryDynamicRepresentationWithDollar() { + var d = new GDX { Id = 1, Data = new Dictionary { { "$a", 1 } } }; + var expected = "{ '_id' : 1, 'Data' : [['$a', 1]] }".Replace("'", "\""); + var json = d.ToJson(); + Assert.AreEqual(expected, json); + + collection.RemoveAll(); + collection.Insert(d); + var r = collection.FindOne(Query.EQ("_id", d.Id)); + Assert.AreEqual(d.Id, r.Id); + Assert.AreEqual(1, r.Data.Count); + Assert.AreEqual(1, r.Data["$a"]); + } + + [Test] + public void TestGenericDictionaryDynamicRepresentationWithDot() { + var d = new GDX { Id = 1, Data = new Dictionary { { "a.b", 1 } } }; + var expected = "{ '_id' : 1, 'Data' : [['a.b', 1]] }".Replace("'", "\""); + var json = d.ToJson(); + Assert.AreEqual(expected, json); + + collection.RemoveAll(); + collection.Insert(d); + var r = collection.FindOne(Query.EQ("_id", d.Id)); + Assert.AreEqual(d.Id, r.Id); + Assert.AreEqual(1, r.Data.Count); + Assert.AreEqual(1, r.Data["a.b"]); + } + + [Test] + public void TestHashtableArrayRepresentationWithDollar() { + var d = new HA { Id = 1, Data = new Hashtable { { "$a", 1 } } }; + var expected = "{ '_id' : 1, 'Data' : [['$a', 1]] }".Replace("'", "\""); + var json = d.ToJson(); + Assert.AreEqual(expected, json); + + collection.RemoveAll(); + collection.Insert(d); + var r = collection.FindOne(Query.EQ("_id", d.Id)); + Assert.AreEqual(d.Id, r.Id); + Assert.AreEqual(1, r.Data.Count); + Assert.AreEqual(1, r.Data["$a"]); + } + + [Test] + public void TestHashtableArrayRepresentationWithDot() { + var d = new HA { Id = 1, Data = new Hashtable { { "a.b", 1 } } }; + var expected = "{ '_id' : 1, 'Data' : [['a.b', 1]] }".Replace("'", "\""); + var json = d.ToJson(); + Assert.AreEqual(expected, json); + + collection.RemoveAll(); + collection.Insert(d); + var r = collection.FindOne(Query.EQ("_id", d.Id)); + Assert.AreEqual(d.Id, r.Id); + Assert.AreEqual(1, r.Data.Count); + Assert.AreEqual(1, r.Data["a.b"]); + } + + [Test] + public void TestHashtableDocumentRepresentationWithDollar() { + var d = new HD { Id = 1, Data = new Hashtable { { "$a", 1 } } }; + var expected = "{ '_id' : 1, 'Data' : { '$a' : 1 } }".Replace("'", "\""); + var json = d.ToJson(); + Assert.AreEqual(expected, json); + + Assert.Throws(() => { collection.Insert(d); }); + } + + [Test] + public void TestHashtableDocumentRepresentationWithDot() { + var d = new HD { Id = 1, Data = new Hashtable { { "a.b", 1 } } }; + var expected = "{ '_id' : 1, 'Data' : { 'a.b' : 1 } }".Replace("'", "\""); + var json = d.ToJson(); + Assert.AreEqual(expected, json); + + Assert.Throws(() => { collection.Insert(d); }); + } + + [Test] + public void TestHashtableDynamicRepresentationNormal() { + var d = new HX { Id = 1, Data = new Hashtable { { "abc", 1 } } }; + var expected = "{ '_id' : 1, 'Data' : { 'abc' : 1 } }".Replace("'", "\""); + var json = d.ToJson(); + Assert.AreEqual(expected, json); + + collection.RemoveAll(); + collection.Insert(d); + var r = collection.FindOne(Query.EQ("_id", d.Id)); + Assert.AreEqual(d.Id, r.Id); + Assert.AreEqual(1, r.Data.Count); + Assert.AreEqual(1, r.Data["abc"]); + } + + [Test] + public void TestHashtableDynamicRepresentationWithDollar() { + var d = new HX { Id = 1, Data = new Hashtable { { "$a", 1 } } }; + var expected = "{ '_id' : 1, 'Data' : [['$a', 1]] }".Replace("'", "\""); + var json = d.ToJson(); + Assert.AreEqual(expected, json); + + collection.RemoveAll(); + collection.Insert(d); + var r = collection.FindOne(Query.EQ("_id", d.Id)); + Assert.AreEqual(d.Id, r.Id); + Assert.AreEqual(1, r.Data.Count); + Assert.AreEqual(1, r.Data["$a"]); + } + + [Test] + public void TestHashtableDynamicRepresentationWithDot() { + var d = new HX { Id = 1, Data = new Hashtable { { "a.b", 1 } } }; + var expected = "{ '_id' : 1, 'Data' : [['a.b', 1]] }".Replace("'", "\""); + var json = d.ToJson(); + Assert.AreEqual(expected, json); + + collection.RemoveAll(); + collection.Insert(d); + var r = collection.FindOne(Query.EQ("_id", d.Id)); + Assert.AreEqual(d.Id, r.Id); + Assert.AreEqual(1, r.Data.Count); + Assert.AreEqual(1, r.Data["a.b"]); + } + } +}