将 C# 类 序列化为 MongoDB 而不在子文档中使用鉴别器
Serializing C# classes to MongoDB without using discriminators in subdocuments
我正在编写 C# 代码来写入现有 Web 应用程序使用的 Mongo 数据库(用 PHP 编写),因此我不需要更改数据库的现有结构。数据库结构如下所示:
{
"_id": ObjectId("5572ee670e86b8ec0ed82c61")
"name": "John Q. Example",
"guid": "12345678-1234-5678-abcd-fedcba654321",
"recordIsDeleted": false,
"address":
{
"line1": "123 Main St.",
"city": "Exampleville"
}
}
我将其读入 class,看起来像这样:
public class Person : MongoMappedBase
{
public ObjectId Id { get; set; }
public Guid Guid { get; set; }
public bool RecordIsDeleted { get; set; }
public string Name { get; set; }
public AddressData Address { get; set; }
// etc.
}
public class AddressData : MongoMappedBase
{
public string Line1 { get; set; }
public string City { get; set; }
// etc.
}
阅读代码如下:
var collection = db.GetCollection<Person>("people");
List<Person> people = collection.Find<Person>(_ => true).ToListAsync().Result;
(注意:我还在开发中。在生产中,我将切换到 ToCursorAsync()
并一次一个地循环数据,所以不要担心我正在将整个列表拉入内存。)
到目前为止,还不错。
然而,当我写出数据时,它是这样的:
{
"_id": ObjectId("5572ee670e86b8ec0ed82c61")
"name": "John Q. Example",
"guid": "12345678-1234-5678-abcd-fedcba654321",
"recordIsDeleted": false,
"address":
{
"_t": "MyApp.MyNamespace.AddressData, MyApp",
"_v":
{
"line1": "123 Main St.",
"city": "Exampleville"
}
}
}
请注意 address
字段看起来有何不同。 这不是我想要的。 我希望地址数据看起来就像地址数据输入(没有 _t
或 _v
字段)。换句话说,最终作为 _v
的 contents 的部分是我想作为 [=18] 的值保留到 Mongo 数据库的部分=] 字段.
现在,如果我只是从我自己的 C# 代码中使用 Mongo 数据库,这可能没问题:如果我要反序列化这个数据结构,我假设(虽然我还没有验证) Mongo 将使用 _t
和 _v
字段创建正确类型 (AddressData
) 的实例,并将它们放入 Address
属性 我的 Person
个实例。那样的话,就万事大吉了。
但我正在与一个 PHP 网络应用程序共享此数据库,该应用程序 不 期望看到那些 _t
和 _v
值在地址数据中,并且不知道如何处理它们。我需要告诉 Mongo "Please do not serialize the type of the Address
property. Just assume that it's always going to be an AddressData
instance, and just serialize its contents without any discriminators."
我目前用来将对象持久化到 Mongo 的代码如下所示:
public UpdateDefinition<TDocument> BuildUpdate<TDocument>(TDocument doc) {
var builder = Builders<TDocument>.Update;
UpdateDefinition<TDocument> update = null;
foreach (PropertyInfo prop in typeof(TDocument).GetProperties())
{
if (prop.PropertyType == typeof(MongoDB.Bson.ObjectId))
continue; // Mongo doesn't allow changing Mongo IDs
if (prop.GetValue(doc) == null)
continue; // If we didn't set a value, don't change existing one
if (update == null)
update = builder.Set(prop.Name, prop.GetValue(doc));
else
update = update.Set(prop.Name, prop.GetValue(doc));
}
return update;
}
public void WritePerson(Person person) {
var update = BuildUpdate<Person>(person);
var filter = Builders<Person>.Filter.Eq(
"guid", person.Guid.ToString()
);
var collection = db.GetCollection<Person>("people");
var updateResult = collection.FindOneAndUpdateAsync(
filter, update
).Result;
}
在那里的某个地方,我需要告诉 Mongo "I don't care about the _t
field on the Address
property, and I don't even want to see it. I know what type of objects I'm persisting into this field, and they'll always be the same." 但是我还没有在 Mongo 文档中找到任何告诉我如何做的东西。有什么建议吗?
我明白了。我确实遇到了 https://groups.google.com/forum/#!topic/mongodb-user/QGctV4Hbipk 中描述的问题,其中 Mongo 需要一个基类型,但被赋予了一个派生类型。根据我上面的代码,预期的基本类型 Mongo 实际上是 object
!我发现 builder.Set()
实际上是一个泛型方法,builder.Set<TField>
,它可以从它的第二个参数(字段数据)的类型中找出它的 TField
类型参数。由于我使用的是 prop.GetValue()
,其中 returns object
,Mongo 期望我的 Address
字段(以及其他字段)上有一个 object
实例我遗漏了问题),因此在所有这些字段上放置 _t
。
答案是显式转换从 prop.GetValue()
返回的对象,以便 builder.Set()
可以调用正确的泛型方法(builder.Set<AddressData>()
而不是 builder.Set<object>()
)这个案例。以下有点难看(我希望有一种方法可以在运行时通过反射获得特定的通用函数重载,因为我可以将整个 switch
语句转换为单个基于反射的方法调用),但它工作:
public UpdateDefinition<TDocument> BuildUpdate<TDocument>(TDocument doc) {
var builder = Builders<TDocument>.Update;
var updates = new List<UpdateDefinition<TDocument>>();
foreach (PropertyInfo prop in typeof(TDocument).GetProperties())
{
if (prop.PropertyType == typeof(MongoDB.Bson.ObjectId))
continue; // Mongo doesn't allow changing Mongo IDs
if (prop.GetValue(doc) == null)
continue; // If we didn't set a value, don't change existing one
switch (prop.PropertyType.Name) {
case "AddressData":
updates.add(builder.Set(prop.Name, (AddressData)prop.GetValue(doc)));
break;
// Etc., etc. Many other type names here
default:
updates.add(builder.Set(prop.Name, prop.GetValue(doc)));
break;
}
}
return builder.Combine(updates);
}
这导致 Address
字段,以及我在真实代码中遇到问题的所有其他字段,在没有任何 _t
或 _v
字段的情况下被持久化,就像我想要。
感谢@rmunn 提出这个问题,对我帮助很大
当我发现这个问答时,我正在为同样的问题而苦苦挣扎。进一步挖掘后,我发现您可以使用 BsonDocumentWrapper.Create()
删除接受的答案中的 switch
语句。这是我找到 tip.
的 link
这里有一个例子供其他人参考:
public UpdateDefinition<TDocument> BuildUpdate<TDocument>(TDocument doc) {
var builder = Builders<TDocument>.Update;
var updates = new List<UpdateDefinition<TDocument>>();
foreach (PropertyInfo prop in typeof(TDocument).GetProperties())
{
if (prop.PropertyType == typeof(MongoDB.Bson.ObjectId))
continue; // Mongo doesn't allow changing Mongo IDs
if (prop.GetValue(doc) == null)
continue; // If we didn't set a value, don't change existing one
updates.add(builder.Set(prop.Name, BsonDocumentWrapper.Create(prop.PropertyType, prop.GetValue(doc))));
}
return builder.Combine(updates);
}
您可以将您的对象转换为 JSON 字符串,然后您可以从该 JSON 字符串转换回 BsonArray(如果是列表)或 BsonDocument(如果是对象)
您要更新的对象
public UpdateDefinition<T> getUpdate(T t)
{
PropertyInfo[] props = typeof(T).GetProperties();
UpdateDefinition<T> update = null;
foreach (PropertyInfo prop in props)
{
if (t.GetType().GetProperty(prop.Name).PropertyType.Name == "List`1")
{
update = Builders<T>.Update.Set(prop.Name, BsonSerializer.Deserialize<BsonArray>(JsonConvert.SerializeObject(t.GetType().GetProperty(prop.Name).GetValue(t))));
}
else if (t.GetType().GetProperty(prop.Name).PropertyType.Name == "object")
{
/* if its object */
update = Builders<T>.Update.Set(prop.Name, BsonSerializer.Deserialize<BsonDocument>(JsonConvert.SerializeObject(t.GetType().GetProperty(prop.Name).GetValue(t))));
}
else
{
/*if its primitive data type */
update = Builders<T>.Update.Set(prop.Name, t.GetType().GetProperty(prop.Name).GetValue(t));
}
}
return update;
}
这将更新任何类型的对象列表,您只需要传递对象
我正在编写 C# 代码来写入现有 Web 应用程序使用的 Mongo 数据库(用 PHP 编写),因此我不需要更改数据库的现有结构。数据库结构如下所示:
{
"_id": ObjectId("5572ee670e86b8ec0ed82c61")
"name": "John Q. Example",
"guid": "12345678-1234-5678-abcd-fedcba654321",
"recordIsDeleted": false,
"address":
{
"line1": "123 Main St.",
"city": "Exampleville"
}
}
我将其读入 class,看起来像这样:
public class Person : MongoMappedBase
{
public ObjectId Id { get; set; }
public Guid Guid { get; set; }
public bool RecordIsDeleted { get; set; }
public string Name { get; set; }
public AddressData Address { get; set; }
// etc.
}
public class AddressData : MongoMappedBase
{
public string Line1 { get; set; }
public string City { get; set; }
// etc.
}
阅读代码如下:
var collection = db.GetCollection<Person>("people");
List<Person> people = collection.Find<Person>(_ => true).ToListAsync().Result;
(注意:我还在开发中。在生产中,我将切换到 ToCursorAsync()
并一次一个地循环数据,所以不要担心我正在将整个列表拉入内存。)
到目前为止,还不错。
然而,当我写出数据时,它是这样的:
{
"_id": ObjectId("5572ee670e86b8ec0ed82c61")
"name": "John Q. Example",
"guid": "12345678-1234-5678-abcd-fedcba654321",
"recordIsDeleted": false,
"address":
{
"_t": "MyApp.MyNamespace.AddressData, MyApp",
"_v":
{
"line1": "123 Main St.",
"city": "Exampleville"
}
}
}
请注意 address
字段看起来有何不同。 这不是我想要的。 我希望地址数据看起来就像地址数据输入(没有 _t
或 _v
字段)。换句话说,最终作为 _v
的 contents 的部分是我想作为 [=18] 的值保留到 Mongo 数据库的部分=] 字段.
现在,如果我只是从我自己的 C# 代码中使用 Mongo 数据库,这可能没问题:如果我要反序列化这个数据结构,我假设(虽然我还没有验证) Mongo 将使用 _t
和 _v
字段创建正确类型 (AddressData
) 的实例,并将它们放入 Address
属性 我的 Person
个实例。那样的话,就万事大吉了。
但我正在与一个 PHP 网络应用程序共享此数据库,该应用程序 不 期望看到那些 _t
和 _v
值在地址数据中,并且不知道如何处理它们。我需要告诉 Mongo "Please do not serialize the type of the Address
property. Just assume that it's always going to be an AddressData
instance, and just serialize its contents without any discriminators."
我目前用来将对象持久化到 Mongo 的代码如下所示:
public UpdateDefinition<TDocument> BuildUpdate<TDocument>(TDocument doc) {
var builder = Builders<TDocument>.Update;
UpdateDefinition<TDocument> update = null;
foreach (PropertyInfo prop in typeof(TDocument).GetProperties())
{
if (prop.PropertyType == typeof(MongoDB.Bson.ObjectId))
continue; // Mongo doesn't allow changing Mongo IDs
if (prop.GetValue(doc) == null)
continue; // If we didn't set a value, don't change existing one
if (update == null)
update = builder.Set(prop.Name, prop.GetValue(doc));
else
update = update.Set(prop.Name, prop.GetValue(doc));
}
return update;
}
public void WritePerson(Person person) {
var update = BuildUpdate<Person>(person);
var filter = Builders<Person>.Filter.Eq(
"guid", person.Guid.ToString()
);
var collection = db.GetCollection<Person>("people");
var updateResult = collection.FindOneAndUpdateAsync(
filter, update
).Result;
}
在那里的某个地方,我需要告诉 Mongo "I don't care about the _t
field on the Address
property, and I don't even want to see it. I know what type of objects I'm persisting into this field, and they'll always be the same." 但是我还没有在 Mongo 文档中找到任何告诉我如何做的东西。有什么建议吗?
我明白了。我确实遇到了 https://groups.google.com/forum/#!topic/mongodb-user/QGctV4Hbipk 中描述的问题,其中 Mongo 需要一个基类型,但被赋予了一个派生类型。根据我上面的代码,预期的基本类型 Mongo 实际上是 object
!我发现 builder.Set()
实际上是一个泛型方法,builder.Set<TField>
,它可以从它的第二个参数(字段数据)的类型中找出它的 TField
类型参数。由于我使用的是 prop.GetValue()
,其中 returns object
,Mongo 期望我的 Address
字段(以及其他字段)上有一个 object
实例我遗漏了问题),因此在所有这些字段上放置 _t
。
答案是显式转换从 prop.GetValue()
返回的对象,以便 builder.Set()
可以调用正确的泛型方法(builder.Set<AddressData>()
而不是 builder.Set<object>()
)这个案例。以下有点难看(我希望有一种方法可以在运行时通过反射获得特定的通用函数重载,因为我可以将整个 switch
语句转换为单个基于反射的方法调用),但它工作:
public UpdateDefinition<TDocument> BuildUpdate<TDocument>(TDocument doc) {
var builder = Builders<TDocument>.Update;
var updates = new List<UpdateDefinition<TDocument>>();
foreach (PropertyInfo prop in typeof(TDocument).GetProperties())
{
if (prop.PropertyType == typeof(MongoDB.Bson.ObjectId))
continue; // Mongo doesn't allow changing Mongo IDs
if (prop.GetValue(doc) == null)
continue; // If we didn't set a value, don't change existing one
switch (prop.PropertyType.Name) {
case "AddressData":
updates.add(builder.Set(prop.Name, (AddressData)prop.GetValue(doc)));
break;
// Etc., etc. Many other type names here
default:
updates.add(builder.Set(prop.Name, prop.GetValue(doc)));
break;
}
}
return builder.Combine(updates);
}
这导致 Address
字段,以及我在真实代码中遇到问题的所有其他字段,在没有任何 _t
或 _v
字段的情况下被持久化,就像我想要。
感谢@rmunn 提出这个问题,对我帮助很大
当我发现这个问答时,我正在为同样的问题而苦苦挣扎。进一步挖掘后,我发现您可以使用 BsonDocumentWrapper.Create()
删除接受的答案中的 switch
语句。这是我找到 tip.
这里有一个例子供其他人参考:
public UpdateDefinition<TDocument> BuildUpdate<TDocument>(TDocument doc) {
var builder = Builders<TDocument>.Update;
var updates = new List<UpdateDefinition<TDocument>>();
foreach (PropertyInfo prop in typeof(TDocument).GetProperties())
{
if (prop.PropertyType == typeof(MongoDB.Bson.ObjectId))
continue; // Mongo doesn't allow changing Mongo IDs
if (prop.GetValue(doc) == null)
continue; // If we didn't set a value, don't change existing one
updates.add(builder.Set(prop.Name, BsonDocumentWrapper.Create(prop.PropertyType, prop.GetValue(doc))));
}
return builder.Combine(updates);
}
您可以将您的对象转换为 JSON 字符串,然后您可以从该 JSON 字符串转换回 BsonArray(如果是列表)或 BsonDocument(如果是对象)
您要更新的对象
public UpdateDefinition<T> getUpdate(T t)
{
PropertyInfo[] props = typeof(T).GetProperties();
UpdateDefinition<T> update = null;
foreach (PropertyInfo prop in props)
{
if (t.GetType().GetProperty(prop.Name).PropertyType.Name == "List`1")
{
update = Builders<T>.Update.Set(prop.Name, BsonSerializer.Deserialize<BsonArray>(JsonConvert.SerializeObject(t.GetType().GetProperty(prop.Name).GetValue(t))));
}
else if (t.GetType().GetProperty(prop.Name).PropertyType.Name == "object")
{
/* if its object */
update = Builders<T>.Update.Set(prop.Name, BsonSerializer.Deserialize<BsonDocument>(JsonConvert.SerializeObject(t.GetType().GetProperty(prop.Name).GetValue(t))));
}
else
{
/*if its primitive data type */
update = Builders<T>.Update.Set(prop.Name, t.GetType().GetProperty(prop.Name).GetValue(t));
}
}
return update;
}
这将更新任何类型的对象列表,您只需要传递对象