使用成员编号保存文档,而不是使用 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 =&gt; 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 的名称的主要内容,如果未能解决你的问题,请参考以下文章

如何继承protobuf生成的C++类

在servicestack中使用protobuf,为啥order只能从1而不是0开始?

使用 protobuf 枚举值作为字段编号

Protobuf-net 包括不可序列化基类的特定成员

如何在查询中使用 $push 将数据插入子文档,而不是检索文档并将其保存回来

使用 protobuf-net 序列化具有接口类型成员的类