1

我在某处看到使用 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 run
10:标题
20:内容
30:类别

4

3 回答 3

1

protobuf-net 的字段元数据可从RuntimeTypeModelAPI 获得,例如:

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];
于 2021-09-09T17:14:52.777 回答
1

重新编辑;在这个地区:

var members = RuntimeTypeModel.Default[typeof(T)].GetFields();
foreach (var member in members)
{
    memberMap.SetElementName(member.FieldNumber.ToString());
}

相信您的目的是确定相关领域memberMap- 即在这种情况下您当时只谈论一个领域;我怀疑正在发生的事情是,对于每个成员,您依次更改元素名称多次,将其保留在定义的最后一个 protobuf 字段中。

另外,还有继承的复杂性;protobuf-net 没有以扁平的方式实现继承——相反,基类型应该是 a[ProtoContract]并且旨在[ProtoInclude(...)]为每个派生类型定义 a ;字段编号是类型特定的,意思是:基类型和派生类型都可以合法地具有字段1。如果您需要描述继承,并且您确定使用protobuf-net的模型,那么您需要处理这个; 例如,您可以使用[ProtoInclude(...)]数字作为每个的前缀,所以Base.Idis "1",如果我们想象在 中有Todo字段,那么可能是。5[ProtoInclude(...)]Todo.Title"5.10"

或者:如果您没有积极使用 protobuf-net:也许只是使用您自己的数字属性?或者您选择的序列化程序通常会直接使用一个内置属性。

于 2021-09-10T06:30:41.847 回答
0

现在好了!因此,经过一番调查,我最终在 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. 当您在数据库中插入一个新文档时,_t如果您使用,则会添加一个字段Discriminator(我不确定它是否完全相关)。基本上,每个以下划线开头的成员都是保留的。运行 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);
    ...         

上面的最后一个是带有"10"属性编号 () 的查询Title。但是可以以相同的方式使用属性名称进行查询,如下所示:

// 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 的第二个解决方案的用武之地,您可以在其中添加前缀...我将默认保留名称约定。

干杯!

于 2021-09-11T03:57:57.787 回答