MongoDB - Cannot create field 'ChildProperty' in element {ParentProperty: is null} - c#

I have a User class:
public class User
{
public Guid? Id { get; set; }
public String? Name { get; set; }
public Address? Address { get; set; }
}
and Address class:
public class Address
{
public String? Street { get; set; }
public String? City { get; set; }
public String? State { get; set; }
}
I am trying to implement partial updating. This is done dynamically, but let's say someone sends to update a User's City and State, I do:
var filter = Builders<BsonDocument>.Filter.Eq("Id", id);
var updates = new List<UpdateDefinition<BsonDocument>>();
updates.Add(Builders<BsonDocument>.Update.Set("Address.City", "New City Value"));
updates.Add(Builders<BsonDocument>.Update.Set("Address.State", "New State Value"));
var update = Builders<BsonDocument>.Update.Combine(updates);
var bsonDocument = await collection.FindOneAndUpdateAsync(filter, update, new FindOneAndUpdateOptions<BsonDocument>
{
ReturnDocument = ReturnDocument.After
});
This works well, except if a User's Address is null. In that case, I get the error:
MongoDB.Driver.MongoCommandException: Command findAndModify failed:
Cannot create field 'City' in element {Address: null}.
Is there any way to ensure the Address object is created so that the City and State properties get set? I would like to do it without getting the current object from the database.

Think that you may work with update with aggregation pipeline to update the Address field dynamically.
Work with $cond operator to check whether Address is null.
If yes, set the whole object to Address.
If no, merge the current Address value with the document.
The query below may looks complex:
db.collection.update({
Id: /* Id */
},
[
{
$set: {
Address: {
$cond: {
if: {
$eq: [
"$Address",
null
]
},
then: {
"City": "New City Value",
"State": "New State Value"
},
else: {
$mergeObjects: [
"$Address",
{
"City": "New City Value",
"State": "New State Value"
}
]
}
}
}
}
}
])
var addressDocument = new BsonDocument
{
{ "City", "New City Value" },
{ "State", "New State Value" }
};
var update = Builders<BsonDocument>.Update.Pipeline(new PipelineStagePipelineDefinition<BsonDocument, BsonDocument>
(
new PipelineStageDefinition<BsonDocument, BsonDocument>[]
{
new BsonDocument("$set",
new BsonDocument("Address",
new BsonDocument("$cond",
new BsonDocument
{
{
"if",
new BsonDocument("$eq",
BsonArray.Create(new object[] { "$Address", null }))
},
{
"then",
addressDocument
},
{
"else",
new BsonDocument("$mergeObjects",
BsonArray.Create(new object[] { "$Address", addressDocument }))
}
}
)
)
)
}
));

Related

How do I select an object from a list MongoDB MVC

I have the following data structure:
[
{
"id": "604ab2c4a568b9181987c9eb",
"name": "Paul",
"eventDate": "2021-03-12T00:16:03.672Z",
"created": "2021-03-12T00:16:03.673Z",
"images": [
{
"id": "604ab2c3a568b9181987c603",
"name": "DSC_0",
"url": "https://picsum.photos/300/300",
"isSelected": true
},
...
]
},
{
"id": "604ab3889c5ac2289b450e3c",
"name": "Paul",
"eventDate": "2021-03-12T00:16:03.672Z",
"created": "2021-03-12T00:16:03.673Z",
"images": [
{
"id": "604ab3879c5ac2289b450a54",
"name": "DSC_0",
"url": "https://picsum.photos/300/300",
"isSelected": true
},
...
]
},
...
]
and im trying to get the image by the user id and the image id, but i cant for some reason
var filter = Builders<Client>.Filter.Eq("Id", "604ab2c4a568b9181987c9eb");
filter &= Builders<Client>.Filter.ElemMatch(u => u.Images,t => t.Id == "604ab2c3a568b9181987c603");
return _client.Find(filter).FirstOrDefault();
This is giving me back all the document with the id "604ab2c4a568b9181987c9eb", but i just wanted to receive the image with the id "604ab2c3a568b9181987c603"
Here the models of the objects:
public class Client
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; }
[BsonElement("name")]
public string Name { get; set; }
[BsonElement("eventDate")]
[BsonDateTimeOptions]
public DateTime EventDate { get; set; }
[BsonElement("created")]
[BsonDateTimeOptions]
public DateTime Created { get; set; }
[BsonElement("images")]
public List<Image> Images { get; set; }
}
public class Image
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; }
[BsonElement("name")]
public string Name { get; set; }
[BsonElement("url")]
[DataType(DataType.ImageUrl)]
public string Url { get; set; }
[BsonElement("isSelected")]
public bool IsSelected { get; set; }
}
So i wanted to know how could i fix my code to make it give me back just an image, or more than one images from the user im searching.
Last but not least, i wanted to know if i can change the isSelected value even if im using the same filter.
You wanted to use projection for only asking for the specific Image with the given id. So the query would look like>
var client = new MongoClient();
var database = client.GetDatabase("test");
var collection = database.GetCollection<Client>("clients");
//collection.InsertOne(new Client
//{
// Created = DateTime.Now,
// EventDate = DateTime.Now,
// Name = "myClient",
// Images = new List<Image>
// {
// new Image
// {
// Id = ObjectId.GenerateNewId(DateTime.Now).ToString(),
// Name = "Image1",
// IsSelected = true,
// Url = "myUrl1"
// },
// new Image
// {
// Id = ObjectId.GenerateNewId(DateTime.Now).ToString(),
// Name = "Image2",
// IsSelected = true,
// Url = "myUrl2"
// }
// }
//});
var filter = Builders<Client>.Filter.Eq("Id", "605a60a76b25fefcc3b1bc00");
var imageFilter = Builders<Image>.Filter.Eq("Id", "605a60a76b25fefcc3b1bbff");
var projection = (ProjectionDefinition<Client, Client>)Builders<Client>.Projection.ElemMatch("Images", imageFilter);
var image = collection.Find(filter).Project(projection).FirstOrDefault() // here you have the specific client with only the right Image in the Images. Other props are default.
?.Images?[0]; // selecting the only Image in the list.
Sorry, but the update part is not that simple. Taking a whole different approach>
var filter = Builders<Client>.Filter.Eq(x => x.Id, "605a60a76b25fefcc3b1bc00");
var update = Builders<Client>.Update.Set("images.$[f].isSelected", false);
var arrayFilters = new[]
{
new BsonDocumentArrayFilterDefinition<BsonDocument>(
new BsonDocument("f._id", BsonObjectId.Create("605a60a76b25fefcc3b1bbff")))
};
collection.UpdateOne(filter, update, new UpdateOptions { ArrayFilters = arrayFilters });
Mostly taken from https://kevsoft.net/2020/03/23/updating-arrays-in-mongodb-with-csharp.html

MongoDB updateMany set a boolean field using equals

I have an IsDefault field in my model, and I want to set it to true for one document only and false for the others. Is this possible in a single query? I tried
{ $set: { IsDefault: {$eq: [_id, id ]}}}
{ $set: { IsDefault: { $cond: { if: { $eq: [$_id, id] }, then: true, else: false } } } }
and it didn't work. I also tried passing a lambda expression but it won't build either. Any idea if this is possible or do I need to use two seperate updateOne statements to the the current IsDefault to false, and the new one to true?
you can achieve it with an aggregation pipeline update like this:
db.collection.updateMany(
{},
[
{
$set: {
IsDefault: {
$eq: ["$_id", ObjectId("5f65c2fcaf29d00898173d03")]
}
}
}
])
here's a full example using mongodb.entities:
using MongoDB.Entities;
using System.Threading.Tasks;
namespace StackOverflow
{
public class Book : Entity
{
public string Title { get; set; }
public bool IsDefault { get; set; }
}
static class Program
{
private static async Task Main()
{
await DB.InitAsync("test", "localhost");
var books = new[] {
new Book { Title = "book 1"},
new Book { Title = "book 2"},
new Book { Title = "book 3"}
};
await books.SaveAsync();
var stage = new Template<Book>(#"
{
$set: {
IsDefault: {
$eq: ['$_id', ObjectId('<idvalue>')]
}
}
}")
.Tag("idvalue", books[1].ID);
await DB.Update<Book>()
.Match(_ => true)
.WithPipelineStage(stage)
.ExecutePipelineAsync();
}
}
}

Writing JSON array in web API response

Need a JSON response like this:
{
"statusCode": 200,
"errorMessage": "Success",
"Employees": [
{
"empid": "7228",
"name": "Tim",
"address": "10815 BALTIMORE",
},
{
"empid": "7240",
"name": "Joe",
"address": "10819 Manasas",
}
]
}
Model Class:
public class EmployeeList
{
public string empid { get; set; }
public string name { get; set; }
public string address { get; set; }
}
public class Employee
{
public int statusCode { get; set; }
public string errorMessage{ get; set; }
public List<EmployeeList> Employees { get; set; }
}
Controller is like this:
List<Models.Employee> emp = new List<Models.Employee>();
//call to SP
if (rdr.HasRows)
{
var okresp = new HttpResponseMessage(HttpStatusCode.OK)
{
ReasonPhrase = "Success"
};
Models.Employee item = new Models.Employee();
{
item.statusCode = Convert.ToInt32(okresp.StatusCode);
item.errorMessage = okresp.ReasonPhrase;
while (rdr.Read())
{
item.Employees = new List<EmployeeList>{
new EmployeeList
{
empid = Convert.ToString(rdr["empid"]),
name = Convert.ToString(rdr["name"]),
address = Convert.ToString(rdr["address"])
}};
}
};
emp.add(item);
// return response
What should be my controller class code to create JSON array response while I read data from reader?
I am not able to get the loop part of creating JSON array in response file. Am I assigning the values in While loop incorrectly?
After your while (rdr.Read()), you reset item.Employees with a new List<EmployeeList> at every data. So you will be getting the last element every time.
Move your list initialisation outside of the loop and use the add method.
item.Employees = new List<EmployeeList>();
while (rdr.Read())
{
item.Employees.Add(
new EmployeeList
{
empid = Convert.ToString(rdr["empid"]),
name = Convert.ToString(rdr["name"]),
address = Convert.ToString(rdr["address"])
});
}

Match "x out of y" fields in a document

I'm writing a service to handle bans for a game and I'm currently a bit stuck trying to write a MongoDB query. Currently I have a collection of "User" objects, and the objects look like this:
public class User
{
public List<Ban> Bans { get; set; }
// some irrelevant additional fields
}
public class Ban
{
public HardwareId HWID { get; set; }
public DateTime Expires { get; set; }
// some irrelevant additional fields
}
public class HardwareId : IEquatable<HardwareId>
{
public string Field1 { get; set; }
public string Field2 { get; set; }
public string Field3 { get; set; }
public string Field4 { get; set; }
public bool Equals([AllowNull] HardwareId other)
{
if (ReferenceEquals(other, null)) return false;
if (ReferenceEquals(this, other)) return true;
return Field1 == other.Field1 &&
Field2 == other.Field2 &&
Field3 == other.Field3 &&
Field4 == other.Field4;
}
}
What I want to do is have a query that finds all users with a ban where HWID has say 3 out of 4 fields matching. Currently I have a query that only finds users where the HWID match exactly (due to the Equals() implementation), but I'd like to change it. My current code looks like this:
public Ban FindBan(HardwareId hwid)
{
var banBuilder = Builders<Ban>.Filter;
var hwidFilter = banBuilder.Eq(b => b.HWID, hwid);
var expFilter = banBuilder.Gt(b => b.Expires, DateTime.UtcNow);
var banFilter = banBuilder.And(hwidFilter, expFilter);
var user = _users.Find(Builders<User>.Filter.ElemMatch(p => p.Bans, banFilter)).FirstOrDefault();
if (user != null)
{
return user.Bans[0];
}
return null;
}
The only way of solving it I can think of is to write "spaghetti if-statements" in the Equals() function, but I'd like a dynamic solution where I could add multiple "Fields" to the HardwareId class later down the line. Another issue with the FindBan() function currently is the fact that it returns "user.Bans[0]" instead of the actual ban that was found, but I imagine I could solve that by sorting by expiry.
here's one approach to match by minimum of 3 fields of HWID:
var numFieldsToMatch = 3;
var hwid = {
Field1: "one",
Field2: "two",
Field3: "three",
Field4: "four"
};
db.User.find({
$expr: {
$anyElementTrue: {
$map: {
input: "$Bans",
in: {
$gte: [{
$size: {
$filter: {
input: { $objectToArray: "$$this.HWID" },
cond: {
$or: [
{ $and: [{ $eq: ["$$this.k", "Field1"] }, { $eq: ["$$this.v", hwid.Field1] }] },
{ $and: [{ $eq: ["$$this.k", "Field2"] }, { $eq: ["$$this.v", hwid.Field2] }] },
{ $and: [{ $eq: ["$$this.k", "Field3"] }, { $eq: ["$$this.v", hwid.Field3] }] },
{ $and: [{ $eq: ["$$this.k", "Field4"] }, { $eq: ["$$this.v", hwid.Field4] }] }
]
}
}
}
}, numFieldsToMatch]
}
}
}
}
})
https://mongoplayground.net/p/MPwB14o6kuO
it's not possible to translate this mongo query to c# driver code afaik.
see here for an easy way to run this with c#.
You can write a method similar to Equals (or make changes to it), so that you have each condition match in to an array
FldArr[0] = (Field1 == other.Field1);
FldArr[1] = (Field2 == other.Field2);
FldArr[2] = (Field3 == other.Field3);
FldArr[3] = (Field4 == other.Field4);
Then by using LINQ for FldArr, you can find 3/4 true conditions.

Update a JSON file with a model object in C#

I have a json file that looks something like :
[
{
"picklist_typ": "Address Assessment Code",
"picklist_typ_key": null,
"picklist_typ_cd": "DELIVERABLE",
"picklist_typ_dsc": "Address is deliverable : Deliverable",
"ref_order": null,
"dw_trans_ts": "2016-07-17T12:59:15"
},
{
"picklist_typ": "Address Assessment Code",
"picklist_typ_key": null,
"picklist_typ_cd": "NOT-DELIVERABLE",
"picklist_typ_dsc": "Address is not deliverable : Undeliverable",
"ref_order": null,
"dw_trans_ts": "2016-07-17T12:59:15"
},
{
"picklist_typ": "Address Type",
"picklist_typ_key": null,
"picklist_typ_cd": "B",
"picklist_typ_dsc": "Billing Address",
"ref_order": null,
"dw_trans_ts": "2016-07-17T12:59:15"
},
....
Now , I have an object that looks like :
public class GroupMembershipWriteOutput
{
public long? groupKey { get; set; }
public string transOutput { get; set; }
public long? transKey { get; set; }
public string groupCode {get;set;}
public string groupName {get;set;}
}
And it has a value accordingly :
groupKey:121
transOutput: "Success"
transKey:998546
groupCode:"My Group Test"
groupName: "My Created Group Test"
What I want to is ..
I want to read the JSON file and if any entry's picklist_typ_key matches the incoming object's groupKey , I want to update only that object's picklist_typ_cd with groupCode
and picklist_typ_dsc with groupName.
I could read the data from JSON as ...
if(gmwo.transOutput.ToUpper() == "SUCCESS")
{
string json = System.Configuration.ConfigurationManager.AppSettings["PicklistDataPath"];
List<PicklistData> deserializedPicklistData = JsonConvert.DeserializeObject<List<PicklistData>>(System.IO.File.ReadAllText(json));
//find the object that matches
IEnumerable<PicklistData> results = deserializedPicklistData.Where(item => item.picklist_typ_key == gmwo.groupKey.ToString());
if(results != null)
{
**//What is the logic to update only that entry in the json file**
}
Please help me do it .
Could you try the LINQ "All" chain? Something like this:
IEnumerable<PicklistData> results = deserializedPicklistData
.Where(item => item.picklist_typ_key == gmwo.groupKey.ToString())
.All(selectedItem => someFunction(selectedItem));
:
:
someFunction(PicklistData myPick) {
:
}

Categories

Resources