I'm trying to use Guid datatype as Id in my Poco object "Parameter". However, while I'm able to write files to the database I can't read from it.
This is the import function writing table headers from a csv file into the database. First line of the csv file are parameters and second line the units those parameters are measured in. All other lines contain actual values and are stored in another collection as BsonDocument. The csv files are dynamic and need to be selectable via combobox, which is why the parameters are written in their own collection.
IMongoCollection<Parameter> parameterCollection = this.MongoDatabase.GetCollection<Parameter>("Parameters");
columnNames.Select((columnName, index) => new Parameter() { Name = columnName, Unit = columnUnits[index] })
.ToList()
.ForEach(parameter =>
{
parameterCollection.UpdateOne(Builders<Parameter>.Filter.Eq("Name", parameter.Name),
Builders<Parameter>.Update.Set("Unit", parameter.Unit),
new UpdateOptions()
{
IsUpsert = true
});
});
This is the Parameter class:
public class Parameter
{
[BsonId]
public Guid Id { get; set; }
public string Name { get; set; }
public string Unit { get; set; }
}
Here's the method trying to read the data from the document:
public List<Parameter> GetParameters()
{
return this.MongoDatabase.GetCollection<Parameter>("Parameters")
.Find(Builders<Parameter>.Filter.Empty)
.ToList();
}
This results in the following error message:
"SystemFormatException: 'An error occurred while deserializing the Id property of class TimeSeriesInterface.DTO.Parameter: Cannot deserialize a 'Guid' from BsonType 'ObjectId'.'
I also tried this attribute: [BsonId(IdGenerator = typeof(GuidGenerator))]
I'm unable to find any help besides those two attributes. They seem to solve it for everybody else, but I still keep getting this error.
I may add that the import and read functions are parts of different classes each calling their own new MongoClient().GetDatabase(MongoDatabaseRepository.DatabaseName); but when I use ObjectId as data type I do get the data so I don't think that's the issue.
Why not use ObjectId as data type? We have an extra project for database access and I do not wish to add the mongodb assembly all over the place just because other projects use the POCOs and require a reference for that pesky little ObjectId.
EDIT:
This is the mapping used within the constructor after suggestion by AlexeyBogdan (beforehand it was simply the call to AutoMap()):
public MongoDatabaseRepository(string connectionString)
{
this.MongoDbClient = new MongoClient();
this.MongoDatabase = this.MongoDbClient.GetDatabase(MongoDatabaseRepository.DatabaseName);
BsonClassMap.RegisterClassMap<Parameter>(parameterMap =>
{
parameterMap.AutoMap();
parameterMap.MapIdMember(parameter => parameter.Id);
});
}
Instead of [BsonId] I recommend you to use this mapping
BsonClassMap.RegisterClassMap<Type>(cm =>
{
cm.AutoMap();
cm.MapIdMember(c => c.Id);
});
I Hope it helps you.
Related
Problem
I am working with the arbitrary data and trying to save and get back data without knowing what the field names and types are. I have a strong-typed model which contains couple of fields and rest of the data is arbitrary.
Arbitrary data comes in an array form and saving the data like this (most of the fields are removed for sake of brevity).
Model:
class TicketModel
{
public int Id { get; set; }
public string test { get; set; }
[BsonExtraElements]
public Dictionary<string,object> Metadata { get; set; }
}
And adding arbitrary data like this;
if (tempTicketDTO.Metadata != null && tempTicketDTO.Metadata .Count > 0)
{
foreach (var item in tempTicketDTO.Metadata )
{
ticketModel.Metadata.Add(item.Name, item.Value);
}
}
Which produces a result like this:
As expected when we query the document like the same way we add;
_db.GetCollection<TicketModel>("ticketModel")
.Find(filter)
.Project(f => new TicketModel { Id = f.Id, Metadata = f.Metadata, test = f.test })
.ToList();
As expected Metadata returns null. Since the dictionary exploded in the document, there is no way to retrieve it back as its saved in the document.
What I am trying to achieve
Save the document with arbitrary data and get back same document as presented in the collection. But I want to able to use project since I don't want to get all of collection.
Using latest mongodb c# driver, .NET Core 3.1, running on docker linux container.
Update: when I directly query database without any projection
_db.GetCollection<TicketModel>("ticketModel").Find(filter).ToList();
It returns like that, but like I mention, I might not want to get for example test field from database.
I've been wanting to deserialize automatically from a Cypher Result into their corresponding objects via neo4jclient.
These are the classes I want it to deserialize into
public class QuestionHub
{
public string Id { get; set; }
public string Title { get; set; }
public ICollection<Question> Questions {get;set;}
}
public class Question
{
public string Id { get; set; }
public string Title { get; set; }
public ICollection<Answer> Answers { get;set; }
}
public class Answer
{
public string Id { get; set; }
public string Value { get; set; }
}
I know for a fact that this code will put the corresponding QuestionHubs into a list of QuestionHubs. This is exactly what I want, but the problem is that those property that navigate to other classes, are not included.
var hubQuery = graphClient.Cypher
.Match("(qh:QuestionHub)-[:PART_OF]-(q:Question)-[:ANSWER]-(a:Answer)")
.ReturnDistinct<QuestionHub>("qh");
This is the
result
As you can see, the questions are not included.
Whenever I do this
var hubQuery = graphClient.Cypher
.Match("(qh:QuestionHub)-[:PART_OF]-(q:Question)-[:ANSWER]-(a:Answer)")
.ReturnDistinct<QuestionHub>("*");
I get the error
System.ArgumentException: 'Neo4j returned a valid response, however Neo4jClient was unable to deserialize into the object structure you supplied.
First, try and review the exception below to work out what broke.
If it's not obvious, you can ask for help at http://stackoverflow.com/questions/tagged/neo4jclient
Include the full text of this exception, including this message, the stack trace, and all of the inner exception details.
Include the full type definition of Neo4JTests.QuestionHub.
Include this raw JSON, with any sensitive values replaced with non-sensitive equivalents:
{ "columns":["a","q","qh"], "data":[[ {"data":{ "Value":"answer9aaf5134-9e73-4681-ba2f-e8224242ff19","Id":"9aaf5134-9e73-4681-ba2f-e8224242ff19" }},{"data":{ "Title":"questiond287a365-364a-4de0-b9f2-574893c1eaaa","Id":"d287a365-364a-4de0-b9f2-574893c1eaaa" }},{"data":{ "Title":"questionHub222a2fbe-6644-491a-b0a1-66df59f05f11","Id":"222a2fbe-6644-491a-b0a1-66df59f05f11" }} ]] } Arg_ParamName_Nam'
Inner Exception
InvalidOperationException: The deserializer is running in single column mode, but the response included multiple columns which indicates a projection instead. If using the fluent Cypher interface, use the overload of Return that takes a lambda or object instead of single string. (The overload with a single string is for an identity, not raw query text: we can't map the columns back out if you just supply raw query text.)
This error is probably because the cypher result gives multiple columns instead of one.
This is what I want to get
If I do this
var hubQuery = graphClient.Cypher
.Match("(u:User)-[:CREATOR_HUB]-(qh:QuestionHub)-[:PART_OF]-(q:Question)-[:ANSWER]-(a:Answer)")
.With("u, qh, q, COLLECT({Id: a.Id, Value: a.Value}) as answers")
.With("u, qh, COLLECT({Id: q.Id, Title: q.Title, Answers:answers}) as questions")
.With("{Creator: {Id:u.Id, FirstName: u.FirstName, LastName: u.LastName}," +
"Id: qh.Id, Title: qh.Title, Questions: questions} as result")
.ReturnDistinct<string>("result");
var hubQueryRes = await hubQuery .ResultsAsync;
List<QuestionHub> hubList = new List<QuestionHub>();
foreach (var hub in hubQueryRes )
{
hubList .Add(JsonConvert.DeserializeObject<QuestionHub>(hub));
}
I get what I want, but I needed to write all those .With
I want a way to automatically do that without all the .With writing.
I'm looking for a way to automatically deserialize the Cypher result into their corresponding objects with nested objects included.
Is there a way to do this?
Thanks in advance!
You're 100% correct that the reason it didn't create your QuestionHub instance is because the return was entirely in the wrong format for the client to cope with.
Unfortunately - your with workaround is about the only way - As you're using it to return the output of Cypher into a format that the Json Deserializer can handle.
The best I can see to do would be this:
var query = gc.Cypher
.Match("(qh:QuestionHub)-[:PART_OF]-(q:Question)-[:ANSWER]-(a:Answer)")
.With("qh, q{.*, Answers: COLLECT(a)} AS qAndA")
.With("qh{.*, Questions: COLLECT(qAndA)} AS result")
.Return(result => result.As<QuestionHub>());
Bear in mind, you would also need your ICollection to be either List or IEnumerable to deserialize properly - deserializing into ICollection isn't supported.
I am stumped on how to save/pass MongoDB UpdateDefinition for logging and later use
I have created general functions for MongoDB in Azure use on a collection for get, insert, delete, update that work well.
The purpose is to be able to have a standard, pre-configured way to interact with the collection. For update especially, the goal is to be able to flexibly pass in an appropriate UpdateDefinition where that business logic is done elsewhere and passed in.
I can create/update/set/combine the UpdateDefinition itself, but when i try to log it by serializing it, it shows null:
JsonConvert.SerializeObject(updateDef)
When I try to log it, save it to another a class or pass it to another function it displays null:
public class Account
{
[BsonElement("AccountId")]
public int AccountId { get; set; }
[BsonElement("Email")]
public string Email { get; set; }
}
var updateBuilder = Builders<Account>.Update;
var updates = new List<UpdateDefinition<Account>>();
//just using one update here for brevity - purpose is there could be 1:many depending on fields updated
updates.Add(updateBuilder.Set(a => a.Email, email));
//Once all the logic and field update determinations are made
var updateDef = updateBuilder.Combine(updates);
//The updateDef does not serialize to string, it displays null when logging.
_logger.LogInformation("{0} - Update Definition: {1}", actionName, JsonConvert.SerializeObject(updateDef));
//Class Created for passing the Account Update Information for Use by update function
public class AccountUpdateInfo
{
[BsonElement("AccountId")]
public int AccountId { get; set; }
[BsonElement("Update")]
public UpdateDefinition<Account> UpdateDef { get; set; }
}
var acct = new AccountUpdateInfo();
acctInfo.UpdateDef = updateDef
//This also logs a null value for the Update Definition field when the class is serialized.
_logger.LogInformation("{0} - AccountUpdateInfo: {1}", actionName, JsonConvert.SerializeObject(acct));
Any thoughts or ideas on what is happening? I am stumped on why I cannot serialize for logging or pass the value in a class around like I would expect
give this a try:
var json = updateDef.Render(
BsonSerializer.SerializerRegistry.GetSerializer<Account>(),
BsonSerializer.SerializerRegistry)
.AsBsonDocument
.ToString();
and to turn a json string back to an update definition (using implicit operator), you can do:
UpdateDefinition<Account> updateDef = json;
this is off the top of my head and untested. the only thing i'm unsure of (without an IDE) is the .Document.ToString() part above.
I'm learning MongoDB and I want to try it with using C#. Is it possible to operate on strongly typed MongoDB documents using C# official MongoDB driver?
I have classes Album and Photo:
public class Album : IEnumerable<Photo>
{
[Required]
[BsonElement("name")]
public string Name { get; set; }
[Required]
[BsonElement("description")]
public string Description { get; set; }
[BsonElement("owner")]
public string Owner { get; set; }
[BsonElement("title_photo")]
public Photo TitlePhoto { get; set; }
[BsonElement("pictures")]
public List<Photo> Pictures { get; set; }
//rest of the class code
}
public class Photo : IEquatable<Photo>
{
[BsonElement("name")]
public string Name;
[BsonElement("description")]
public string Description;
[BsonElement("path")]
public string ServerPath;
//rest of the class code
}
I want to insert a new document into a collection albums in the test database. I don't want to operate on BsonDocument, but I would prefer to use strongly typed Album. I thought it would be something like:
IMongoClient client = new MongoClient();
IMongoDatabase db = client.GetDatabase("test");
IMongoCollection<Album> collection = database.GetCollection<Album>("album");
var document = new Album
{
Name = album.Name,
Owner = HttpContext.Current.User.Identity.Name,
Description = album.Description,
TitlePhoto = album.TitlePhoto,
Pictures = album.Pictures
};
collection.InsertOne(document);
But it gives me the following error:
An exception of type 'MongoDB.Driver.MongoCommandException' occurred
in MongoDB.Driver.Core.dll but was not handled in user code
Additional information: Command insert failed: error parsing element 0
of field documents :: caused by :: wrong type for '0' field, expected
object, found 0: [].
What am I doing wrong and if it's possible to achieve?
It looks like the driver is treating your object as a BSON array because it implements IEnumerable<Photo>. The database is expecting a BSON document instead. You'll get a similar error if you try to insert, for example, an Int32 into a collection.
Unfortunately, I don't know how to configure the serializer to treat your Album object as a BSON document. The static BsonSerializer.SerializerRegistry property shows the driver is choosing to use EnumerableInterfaceImplementerSerializer<Album,Photo> as Album's serializer by default.
Removing the IEnumerable<Photo> implementation from Album causes the driver to serialize with BsonClassMapSerializer<Album>, producing a BSON document. While it works, the downside is Album is no longer enumerable; application consumers will need to enumerate the Pictures property.
Adding the IEnumerable<Photo> implementation back in, then forcing the aforementioned serializer (using the [BsonSerializer(typeof(BsonClassMapSerializer<Album>))] attribute) results in:
System.MissingMethodException: No parameterless constructor defined for this object.
Based on the stack trace (referring to BsonSerializerAttribute.CreateSerializer), the object the message refers to appears to be something serialization-related, not the data objects themselves (I defined parameterless constructors for both). I don't know if there's a way around this problem with further configuration, or if the driver just won't allow an IEnumerable to be used this way.
When using a FindOne() using MongoDB and C#, is there a way to ignore fields not found in the object?
EG, example model.
public class UserModel
{
public ObjectId id { get; set; }
public string Email { get; set; }
}
Now we also store a password in the MongoDB collection, but do not want to bind it to out object above. When we do a Get like so,
var query = Query<UserModel>.EQ(e => e.Email, model.Email);
var entity = usersCollection.FindOne(query);
We get the following error
Element 'Password' does not match any field or property of class
Is there anyway to tell Mongo to ignore fields it cant match with the models?
Yes. Just decorate your UserModel class with the BsonIgnoreExtraElements attribute:
[BsonIgnoreExtraElements]
public class UserModel
{
public ObjectId id { get; set; }
public string Email { get; set; }
}
As the name suggests, the driver would ignore any extra fields instead of throwing an exception. More information here - Ignoring Extra Elements.
Yet Another possible solution, is to register a convention for this.
This way, we do not have to annotate all classes with [BsonIgnoreExtraElements].
Somewhere when creating the mongo client, setup the following:
var pack = new ConventionPack();
pack.Add(new IgnoreExtraElementsConvention(true));
ConventionRegistry.Register("My Solution Conventions", pack, t => true);
Yes. Another way (instead of editing you model class) is to use RegisterClassMap with SetIgnoreExtraElements.
In your case just add this code when you initialize your driver:
BsonClassMap.RegisterClassMap<UserModel>(cm =>
{
cm.AutoMap();
cm.SetIgnoreExtraElements(true);
});
You can read more about ignoring extra elements using class mapping here - Ignoring Extra Elements.