Save document with the member number instead the name with protobuf-net and MongoDB - mongodb

I saw somewhere that with the Go MongoDB driver it is possible to save a document with the order number instead of the field name.
They end up with this in the database:
{
"3": "foo",
"10": 1,
"33": 123456
"107": {
"2": "bar",
"1": "foo"
}
}
I like the idea!
So, I tried to find a way to do the same with the MongoDB C# driver.
I have the code below but I am not sure what I should bring from the protobut-net to get the member order number.
var pack = new ConventionPack();
pack.AddMemberMapConvention("numbered", m => m.SetElementName( WHAT TO PUT HERE ));
ConventionRegistry.Register("numbered", pack, type => true);
The SetElementName takes a string parameter.
How can I grab the order number of a member from protobuf-net?
Something like ...Member.Order.ToString()
I don't know if this whole thing is a great idea but I want to test it.
Thanks
-- UPDATE --
Just to add more information. I am using inheritance for my models to use generics.
[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; }
}
And I added this line as shown in the protobuf-net documentation:
RuntimeTypeModel.Default[typeof(Base)].AddSubType(42, typeof(Todo));
So with that and what Marc showed to get the member's number, I end up having a custom Convention Class in MongoDB with <T> so I can use it for other objects:
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());
}
}
}
And the registration of this Convention is done like so:
var pack = new ConventionPack { new NumberedElementNameConvention<Todo>() };
ConventionRegistry.Register("NumberedName", pack, type => true);
After running this I get this error:
Grpc.AspNetCore.Server.ServerCallHandler[6]
Error when executing service method 'CreateOne'.
MongoDB.Bson.BsonSerializationException: The property 'UpdatedDate' of type 'Nnet.Models.Base' cannot use element name '30' because it is already being used by property 'CreatedDate'...
Also, when I run the code below I am expecting to get all members of the Todo object.
var members = RuntimeTypeModel.Default[typeof(Todo)].GetFields();
foreach (var member in members)
{
Console.WriteLine($"{member.FieldNumber}: {member.Member.Name}");
}
However, I am not getting those inherited from the Base object:
❯ dotnet run
10: Title
20: Content
30: Category

The field metadata for protobuf-net is available from the RuntimeTypeModel API, for example:
var members = RuntimeTypeModel.Default[yourType].GetFields();
foreach (var member in members)
{
Console.WriteLine($"{member.FieldNumber}: {member.Member.Name}");
}
The .FieldNumber gives the protobuf field-number, and .Member gives the MemberInfo of the corresponding field or property. You may want to do some level of caching if the m => m.SetElementName( WHAT TO PUT HERE ) is evaluated lots of times for the same m, so you don't perform unnecessary work - but: before you do, just add some logging to the lambda first, and see how often it gets called: if it isn't too often, maybe don't worry about it.
Note that there is also a lookup on MetaType that allows query by MemberInfo:
var member = RuntimeTypeModel.Default[yourType][memberInfo];

Re the edit; in this region:
var members = RuntimeTypeModel.Default[typeof(T)].GetFields();
foreach (var member in members)
{
memberMap.SetElementName(member.FieldNumber.ToString());
}
I believe you're meant to identify the relevant field from memberMap - i.e. in this context you're only talking about one field at the time; I suspect what is happening is that for each member in turn you're changing the element name multiple times, leaving it at the last protobuf field defined.
Separately, there's a complication of inheritance; protobuf-net doesn't implement inheritance in a flat way - instead, the base type is also expected to be a [ProtoContract] and is meant to define a [ProtoInclude(...)] for each derived type; the field numbers are type-specific, meaning: both the base type and the derived type can legally have a field 1. If you need to describe inheritance, and you are determined to use protobuf-net's model, then you would need to handle this; for example, you could use the [ProtoInclude(...)] number as a prefix on each, so Base.Id is "1", and if we imagine that Todo has field 5 in the [ProtoInclude(...)], then Todo.Title could be "5.10".
Alternatively: if you're not actively using protobuf-net: maybe just use your own attribute for the numbers? or there's usually an inbuilt attribute that the serializer you've chosen would use directly.

Okay now! So after a some investigation I end up with this simple way to do it with Marc's help. In MongoDB instead of using attributes to decorate models and its properties, it is possible to use code within BsonClassMap. Within that class I add the foreach loop that Marc provided and the right parameters, we can now have numbers instead names.
On the Client side and Server side it is this same code:
//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);
}
});
it's a little ugly but you can rework it.
One thing to note is that MongoDB has the control over the Id. In the database anything that represent the object id become _id. Same thing when you insert a new document in the database a _t field is added if you use Discriminator (I am not sure if it's full related). Basically, every member beginning with a underscore is reserved. See the image below after running de code:
You can refer to the question above in the update section to see if this result represent the models with the given orders (it does).
Here is the code I use for insertion and queries:
// 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);
...
The last one above it's a query with the number ("10") of the property Title. But it's possible in the same way to query with the property name, like so:
// FIND ONE BY QUERY FILTER REQUEST
...
var filter = Builders<Todo>.Filter.Eq(e => e.Title, "Some Title");
var filterString = filter.Render(documentSerializer, serializerRegistry);
...
What is great with this approach is that these BsonClassMap are called once on the Client or/and Server when they are initiated.
I just realize that this might not be a good idea because it is going to be painful to prevent collision between numbers. The order numbers in the code below is possible:
[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; }
}
but there is going to be three collisions if the foreach loop runs.
Yeah... :/
This is where Marc's second solution comes in, where you put a prefix... I am going to keep the name convention by default.
Cheers!

Related

How do you map strings in the database to enums in your model without introducing a second property?

My database has a table like this:
Cats
- CatId INT PK
- Name VARCHAR(100)
- FavoriteToy VARCHAR(100)
And my code looks like this:
Cat.cs
public int CatId { get; set; }
public string Name { get; set; }
public Toy FavoriteToy {get; set; }
StaticVariables.cs
public enum Toy { Box, Ball, StuffedAnimal }
In a normalized database I would use a lookup table in the database to store all the toys and then the Cats table would just store a ToyId. But for this situation it's a lot easier to just store the FavoriteToy as a string even though it will be redundant.
The problem is I don't know how to convert a string in the database to an enum in code without creating a second FavoriteToyString property and having FavoriteToy just be a computed that returns the enum derived from FavoriteToyString.
I've heard this might be possible in the current version of entity framework. Is that true? Can you please show me how to do this?
You may use DTO class and automapper to solve your issue :)
Generally, yes a lookup table reference is a better option since your data can comply with referential integrity. That is, No cat records with toys that your Enum hopefully doesn't contain. (Though your Enum would need to be kept in sync with the Toys table.) You can configure EF to store enumerations as a string using a bit of a trick with the mapping:
public class Cat
{
[Key]
public int CatId { get; set; }
public string Name { get; set; }
[Column("FavoriteToy")]
public string FavoriteToyMapped { get; set; }
[NotMapped]
public Toy FavoriteToy
{
get { return (Toy)Enum.Parse(typeof(Toy), FavoriteToyMapped); }
set { FavoriteToyMapped = value.ToString(); }
}
}
The caveat of this approach is that where you might use Linq to Entity to filter on your cat's favorite toy, you need to reference the FavoriteToyMapped value in the query expression because EF/DB won't know what FavoriteToy is.
I.e.
Cats with a favorite toy of "Yarn"
var catsThatLoveYarn = context.Cats.Where(c => c.FavoriteToyMapped == Toys.Yarn.ToString()).ToList();
// not
var catsThatLoveYarn = context.Cats.Where(c => c.FavoriteToy == Toys.Yarn).ToList();
// Will error because EF doesn't map that property.
Once you are working with instances of entities, that the set of entities has been pulled back from the database, you can further access/refine queries with FavoriteToy. Just be cautious and prepared for the unknown field if you use it too early and EF goes and tries to compose a query.
var threeYearOldCats = context.Cats.Where(c => c.Age == 3).ToList();
var threeYearOldCatsThatLoveYarn = threeYearOldCats.Where(c => c.FavoriteToy == Toys.Yarn).ToList();
This is Ok because the .ToList() in the first query executed the EF-to-SQL, so threeYearOldCats is now a local List<Cat> of cat entities, not an IQueryable<Cat>.

EF Core - unincluding binrary fields

I have an EF Core model that has a binary field
class SomeModel {
string Id;
string otherProperty;
byte[] blob;
};
Usually, when I query the DB, I want to return a list of this Model - and then, on subsequent calls, query just a single entity, but return the blob.
I can't see a way in either data or code first to prevent EF Core paying the cost of retrieving the blob field always.
I really want to be able to say something like:
var list = await Context.SomeModels.ToListAsync();
// later
var item = await Context.SomeModels
.Where(m=>m.Id==someId)
.Include(m=>m.blob)
.FirstOrDefaultAsync();
I think I might have to put the blobs into a 2nd table so I can force a optional join.
The only way you could get a separate loading is to move the data to a separate entity with one-to-one relationship.
It doesn't need to be a separate table though. Although the most natural choice looks to be owned entity, since owned entities are always loaded with the owners, it has to be a regular entity, but configured with table splitting - in simple words, share the same table with the principal entity.
Applying it to your sample:
Model:
public class SomeModel
{
public string Id { get; set; }
public string OtherProperty { get; set; }
public SomeModelBlob Blob { get; set; }
};
public class SomeModelBlob
{
public string Id { get; set; }
public byte[] Data { get; set; }
}
Configuration:
modelBuilder.Entity<SomeModelBlob>(builder =>
{
builder.HasOne<SomeModel>().WithOne(e => e.Blob)
.HasForeignKey<SomeModelBlob>(e => e.Id);
builder.Property(e => e.Data).HasColumnName("Blob");
builder.ToTable(modelBuilder.Entity<SomeModel>().Metadata.Relational().TableName);
});
Usage:
Code:
var test = context.Set<SomeModel>().ToList();
SQL:
SELECT [s].[Id], [s].[OtherProperty]
FROM [SomeModel] AS [s]
Code:
var test = context.Set<SomeModel>().Include(e => e.Blob).ToList();
SQL:
SELECT [e].[Id], [e].[OtherProperty], [e].[Id], [e].[Blob]
FROM [SomeModel] AS [e]
(the second e.Id in the select looks strange, but I guess we can live with that)

C# Serialization in MongoDb - _id on nested type, and some properties with private setters

I'm in a position where I need to serialize some complex documents into MongoDb, but I can't change the class definition as I don't have control over the source.
However, we need to ensure that callers can still use Linq, so we need to map the class correclty into MongoDb.
Current there are few issues we're faced with:
The _id_ representation is on a nested class.
There are properties with private setters that need to be serialized/ deserialzied.
The shape of the class looks a little like this:
public class AggregateType : AggregateBase
{
public int IntProperty { get; private set; }
public ComplexObject ComplexObjectProperty { get; private set; }
}
With AggregateBase looking like this:
abstract public class AggregateBase
{
public AggregateDetails Details { get; set; }
}
And finally:
public class AggregateDetails
{
public Guid Id { get; set; }
...other properties
}
On the base class AggregateBase, there is a property called Details which contains the Id of the aggregate, which is a Guid. This Id field needs to be mapped to the ObjectId or _id field within a MongoDb document.
I need to be able to serialize the document, forcing the use of the Details.Id as the _id, and have the private setters serialized too.
I've done this with CosmoDb using a custom JsonContractResolver without issue. But the move to MongoDb has proved a little more complex.
It's worth noting that there are many AggregateType classes, all with a different shape. I'd like to find a generic way of serializing them, without having to write lots of specific mappers if possible - much like we do with CosmoDb.
On top of that, we would need this solution to work with the Linq query provider for MongoDb too.
Ive thought a little about this , the only way I can see this working is if you create matching types that will serve as your POCO for inserting into mongodb. Im going to assume you are using the C# Driver for Mongo.
public class AggregateTypeDocument : AggregateBaseDocument
{
public int IntProperty { get; private set; }
public ComplexObject ComplexObjectProperty { get; private set; }
}
abstract public class AggregateBaseDocument
{
public AggregateDetailsDocument Details { get; private set; }
}
public class AggregateDetailsDocument
{
[BsonId]
public Guid Id { get; private set; }
...other properties
}
You will end up replicating the structure but just be appending Document at the end for this example. By no means do you have to conform to this
Now you can mold your types to be more mongo friendly using various attributes.
The next step would be to either in your repository ( or wherever ) to map the types with class definitions you don't have access to to your new mongo friendly ones.
I would suggest AutoMapper for this or plain old instantiation. Now you should be able to safely operate on the collection. See below example for automapper.
var normalAggregateType = new AggregateType();
var client = new MongoClient("yourconnectionstring");
var db = client.GetDatabase("mydatabase");
var collection = db.GetCollection<AggregateTypeDocument>("myaggregatetypes");
var mongoAggregateType = Mapper.Map<AggregateTypeDocument>(normalAggregateType);
collection.InsertOne(mongoAggregateType);

Many to Many Relationship doesn't work both ways?

I've been trying to grok EF many-to-many relationships for the past two days now and I'm still missing something even after scouring a dozen different questions here.
I've got a model named Text that can have an arbitrary number of Tag models associated with it, and obviously, at least in theory, each Tag can be associated with an arbitrary number of Texts. Entity Framework seems to understand this well enough to create a table named TextTags in the database without me asking it to do so, and I can access Text.Tags without trouble, but when I attempt to access Tag.Texts in my code, I get a null reference exception.
Now, I could just add every text to every tag manually (or could I? that seems to throw some kind of error), but that would seem to defeat the purpose... Besides which, it also seems error prone. What am I failing to understand?
Code as requested:
Text model:
public class Text
{
public int ID { get; set; }
public Author Author { get; set; }
public string Content { get; set; }
public virtual ICollection<Tag> Tags { get; set; }
}
Tag model:
public class Tag
{
public int ID { get; set; }
public string Name { get; set; }
public ICollection<Text> Texts { get; set; }
}
Data insert:
using (var db = new TextDbContext())
{
db.Authors.Add(new Author()
{
Name = "Poe"
});
db.Tags.Add(new Tag() { Name = "lame" });
db.Tags.Add(new Tag() { Name = "example" });
db.SaveChanges();
db.Texts.Add(new Text()
{
Author = db.Authors.First(),
Tags = db.Tags.ToList(),
Content = "This is the first text by Poe."
});
db.Texts.Add(new Text()
{
Author = db.Authors.First(),
Tags = db.Tags.ToList(),
Content = "This is the second text by Poe."
});
db.Texts.Add(new Text()
{
Author = db.Authors.First(),
Tags = db.Tags.ToList(),
Content = "This is the third text by Poe."
});
db.SaveChanges();
}
Error:
foreach (var tag in db.Tags)
{
foreach (var text in tag.Texts)
{
Console.WriteLine("Tag: {0}\tText: {1}", tag.Name, text.Content);
// Null reference on line above.
}
}
You get a NullReferenceException because your navigation property Tag.Texts is not marked as virtual. As a result lazy loading does not work to load the Tag.Texts collection when you access it and the collection is null. (Text.Tags is virtual, hence no exception here.)

Searching the Entity Framework domain model utilising Code First

Got a very difficult EntityFramework Code First question. I'll keep this as simple as possible.
Imagine we have n number of classes, lets start with 2 for now
public class Person
{
public string Name { get; set; }
}
public class Address
{
public string AddressLine1 { get; set; }
public string AddressLine2 { get; set; }
}
Now then, what I want to do is be able to search the domain model with a single string, i.e. something like DbContext.Search( "Foo" ). The call would search both the person and address tables for a string match and would return a list populated with both Person and Address entities.
Have to say I am not entirely clear how to go about it but I am considering using DataAnnotations to do something like this
public class Person
{
**[Searchable]**
public string Name { get; set; }
}
public class Address
{
**[Searchable]**
public string AddressLine1 { get; set; }
**[Searchable]**
public string AddressLine2 { get; set; }
}
Am I on the right track?
Should I use the Fluent API instead?
Reflection?
Any and all thoughts massively appreciated.
the Find method searches only in the Primary Key column. If we don't make any column explicitly primary key column then find method will throw error. Generally EF convention takes propertyName+id as the primary key in the class. But if you want to search with Name then Make add [Key] to the property. it will become primary key and u will be able to find properties.
dbContext.Addresses.find("Foo");
Create a new object type onto which you'll project 2 types of search results:
public class Result
{
public string MainField { get; set; }
// you may have other properties in here.
}
Then find entities of each type that match your criteria, projecting them onto this type:
var personResults = DbContext.Persons
.Where(p => p.Name == "Foo")
.Select(p => new Result{MainField = p.Name});
// don't forget to map to any other properties you have in Result as well
var addressResults = DbContext.Adresses
.Where(a =>
a.AddressLine1 == "Foo" ||
a.AddressLine2 == "Foo"
).
.Select(a => new Result{MainField = a.AddressLine1 + ", " + a.AddressLine2 });
// again, don't forget to map to any other properties in Result
Then merge the lists:
var allResults = personResults.Union(addressResults).ToList();
...at which point you can sort the list however you like.
"Result" and "MainField", are rather generic; just using them because I am not thoroughly aware of your domain model.