使用成员编号保存文档,而不是使用 protobuf-net 和 MongoDB 的名称
Posted
技术标签:
【中文标题】使用成员编号保存文档,而不是使用 protobuf-net 和 MongoDB 的名称【英文标题】:Save document with the member number instead the name with protobuf-net and MongoDB 【发布时间】:2021-11-06 08:08:59 【问题描述】:我在某处看到使用 Go MongoDB 驱动程序可以使用订单号而不是字段名称来保存文档。 他们最终在数据库中得到了这个:
"3": "foo",
"10": 1,
"33": 123456
"107":
"2": "bar",
"1": "foo"
我喜欢这个主意! 所以,我试图找到一种方法来使用 MongoDB C# 驱动程序。 我有下面的代码,但我不确定我应该从 protobut-net 带来什么来获取会员订单号。
var pack = new ConventionPack();
pack.AddMemberMapConvention("numbered", m => m.SetElementName( WHAT TO PUT HERE ));
ConventionRegistry.Register("numbered", pack, type => true);
SetElementName
采用字符串参数。
如何从 protobuf-net 中获取成员的订单号?
类似...Member.Order.ToString()
我不知道这整件事是否是个好主意,但我想测试一下。
谢谢
-- 更新--
只是为了添加更多信息。我正在为我的模型使用继承以使用泛型。
[BsonDiscriminator("Base", RootClass = true)]
[DataContract]
public abstract class Base
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
[ProtoMember(1)]
public string Id get; set;
[BsonDateTimeOptions]
[ProtoMember(2)]
public DateTime CreatedDate get; private set; = DateTime.UtcNow;
[BsonDateTimeOptions]
[ProtoMember(3)]
public DateTime UpdatedDate get; set; = DateTime.UtcNow;
[ProtoContract]
public class Todo : Base
[ProtoMember(10)]
public string Title get; set;
[ProtoMember(20)]
public string Content get; set;
[ProtoMember(30)]
public string Category get; set;
我添加了这一行,如 protobuf-net 文档中所示:
RuntimeTypeModel.Default[typeof(Base)].AddSubType(42, typeof(Todo));
因此,有了这个以及 Marc 为获取成员编号而展示的内容,我最终在 MongoDB 中拥有了一个带有 <T>
的自定义约定类,因此我可以将它用于其他对象:
public class NumberedElementNameConvention<T> : ConventionBase, IMemberMapConvention where T : Base
public void Apply(BsonMemberMap memberMap)
var members = RuntimeTypeModel.Default[typeof(T)].GetFields();
foreach (var member in members)
memberMap.SetElementName(member.FieldNumber.ToString());
而本公约的注册是这样完成的:
var pack = new ConventionPack new NumberedElementNameConvention<Todo>() ;
ConventionRegistry.Register("NumberedName", pack, type => true);
运行后我得到这个错误:
Grpc.AspNetCore.Server.ServerCallHandler[6] 执行服务方法“CreateOne”时出错。 MongoDB.Bson.BsonSerializationException:类型为“Nnet.Models.Base”的属性“UpdatedDate”不能使用元素名称“30”,因为它已被属性“CreatedDate”使用...
另外,当我运行下面的代码时,我希望得到 Todo 对象的所有成员。
var members = RuntimeTypeModel.Default[typeof(Todo)].GetFields();
foreach (var member in members)
Console.WriteLine($"member.FieldNumber: member.Member.Name");
但是,我没有得到从 Base 对象继承的那些:
❯ dotnet 运行 10:标题 20:内容 30:类别
【问题讨论】:
【参考方案1】:protobuf-net 的字段元数据可从RuntimeTypeModel
API 获得,例如:
var members = RuntimeTypeModel.Default[yourType].GetFields();
foreach (var member in members)
Console.WriteLine($"member.FieldNumber: member.Member.Name");
.FieldNumber
给出 protobuf 字段编号,.Member
给出相应字段或属性的MemberInfo
。如果m => m.SetElementName( WHAT TO PUT HERE )
对同一个m
进行多次评估,您可能需要进行某种程度的缓存,这样您就不会执行不必要的工作 - 但是:在您这样做之前,只需先向 lambda 添加一些日志记录,看看它被调用的频率:如果不是太频繁,也许不用担心。
请注意,MetaType
上还有一个查询,允许通过 MemberInfo
进行查询:
var member = RuntimeTypeModel.Default[yourType][memberInfo];
【讨论】:
谢谢 Marc,我用更多信息更新了我的问题。【参考方案2】:重新编辑;在这个地区:
var members = RuntimeTypeModel.Default[typeof(T)].GetFields();
foreach (var member in members)
memberMap.SetElementName(member.FieldNumber.ToString());
我相信您应该从memberMap
中识别相关字段 - 即在这种情况下您当时只谈论一个字段;我怀疑发生的事情是,对于每个成员,您依次多次更改元素名称,将其保留在定义的最后一个 protobuf 字段中。
另外,还有一个复杂的继承; protobuf-net 没有以扁平的方式实现继承 - 相反,基类型 也 应该是 [ProtoContract]
并且旨在为每个派生类型定义 [ProtoInclude(...)]
;字段编号是特定于类型的,这意味着:基类型和派生类型都可以合法地具有字段 1。如果您需要描述继承,并且确定使用 protobuf-net 的模型,那么你需要处理这个;例如,您可以使用[ProtoInclude(...)]
数字作为每个前缀,因此Base.Id
是"1"
,如果我们想象Todo
在[ProtoInclude(...)]
中有字段5
,那么Todo.Title
可以是"5.10"
。
或者:如果您没有积极使用 protobuf-net:也许只是使用您自己的数字属性?或者通常您选择的序列化程序会直接使用一个内置属性。
【讨论】:
这个想法是通过将名称更改为一个数字来节省一些数据库查询时间。现在,我认为这不值得,因为我可以节省的是花费在必须添加以使其工作的代码和循环块上。我将保留默认值,即以 PascalCase 命名的属性。无论如何,谢谢马克!【参考方案3】:现在好了!因此,经过一番调查,我最终在 Marc 的帮助下找到了这种简单的方法。在 MongoDB 中,可以使用 BsonClassMap
中的代码,而不是使用属性来装饰模型及其属性。在该类中,我添加了 Marc 提供的 foreach 循环和正确的参数,我们现在可以使用数字而不是名称。
在客户端和服务器端是相同的代码:
//Base Model ClassMap
BsonClassMap.RegisterClassMap<Base>(cm =>
cm.AutoMap();
foreach (var member in RuntimeTypeModel.Default[typeof(Base)].GetFields())
cm.MapMember(typeof(Base).GetMember(member.Member.Name)[0])
.SetElementName(member.FieldNumber.ToString())
.SetOrder(member.FieldNumber);
);
//Todo Model ClassMap
BsonClassMap.RegisterClassMap<Todo>(cm =>
cm.AutoMap();
foreach (var member in RuntimeTypeModel.Default[typeof(Todo)].GetFields())
cm.MapMember(typeof(Todo).GetMember(member.Member.Name)[0])
.SetElementName(member.FieldNumber.ToString())
.SetOrder(member.FieldNumber);
);
它有点难看,但你可以重做它。
需要注意的是,MongoDB 可以控制Id
。在数据库中anything that represent the object id
变为_id
。当您在数据库中插入一个新文档时,如果您使用 Discriminator
,则会添加一个 _t
字段(我不确定它是否完全相关)。基本上,每个以下划线开头的成员都是保留的。运行de代码后如下图:
您可以参考更新部分中的上述问题,以查看此结果是否代表具有给定订单的模型(确实如此)。
这是我用于插入和查询的代码:
// INSERT
var client = channel.CreateGrpcService<IBaseService<Todo>>();
var reply = await client.CreateOneAsync(
new Todo
Title = "Some Title"
);
// FIND BY ID
var todoId = new UniqueIdentification Id = "613c110a073055f0d87a0e27";
var res = await client.GetById(todoId);
// FIND ONE BY QUERY FILTER REQUEST
...
var filter = Builders<Todo>.Filter.Eq("10", "Some Title");
var filterString = filter.Render(documentSerializer, serializerRegistry);
...
它上面的最后一个是对属性Title
的编号("10"
) 的查询。但也可以用相同的方式查询属性名称,如下所示:
// FIND ONE BY QUERY FILTER REQUEST
...
var filter = Builders<Todo>.Filter.Eq(e => e.Title, "Some Title");
var filterString = filter.Render(documentSerializer, serializerRegistry);
...
这种方法的优点在于这些BsonClassMap
在启动时会在客户端或/和服务器上调用一次。
我只是意识到这可能不是一个好主意,因为防止数字之间的冲突会很痛苦。下面代码中的订单号是可能的:
[BsonDiscriminator("Base", RootClass = true)]
[DataContract]
public abstract class Base
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
[ProtoMember(1)]
public string Id get; set;
[BsonDateTimeOptions]
[ProtoMember(2)]
public DateTime CreatedDate get; private set; = DateTime.UtcNow;
[BsonDateTimeOptions]
[ProtoMember(3)]
public DateTime UpdatedDate get; set; = DateTime.UtcNow;
[ProtoContract]
public class Todo : Base
[ProtoMember(1)]
public string Title get; set;
[ProtoMember(2)]
public string Content get; set;
[ProtoMember(3)]
public string Category get; set;
但如果foreach
循环运行,将会发生三个冲突。
是的... :/
这就是 Marc 的第二个解决方案的用武之地,您可以在其中添加前缀...我将默认保留名称约定。
干杯!
【讨论】:
以上是关于使用成员编号保存文档,而不是使用 protobuf-net 和 MongoDB 的名称的主要内容,如果未能解决你的问题,请参考以下文章
在servicestack中使用protobuf,为啥order只能从1而不是0开始?