C# MongoDB insert new object to List - c#

I'm new to both C# and MongoDB. I'm trying to add a new object to a list in a MongoDB collection, here is my code:
ProjectModel:
class ProjectModel
{
[BsonId]
public Guid Id { get; set; }
public string ProjectNumber { get; set; }
public string ProjectName { get; set; }
public PartModel[] Parts { get; set; }
}
PartModel:
class PartModel
{
public string PartNumber { get; set; }
public string PartName { get; set; }
}
Method Class
class MongoCRUD
{
private IMongoDatabase db;
public MongoCRUD(string database)
{
var client = new MongoClient();
db = client.GetDatabase(database);
}
public void InsertPart<T>(string table, Guid id, PartModel newPart)
{
var collection = db.GetCollection<T>(table);
var filter = Builders<T>.Filter.Eq("Id", id);
var update = Builders<ProjectModel>.Update.Push<PartModel>(e => e.Parts, newPart);
}
}
Main Class:
class Program
{
static void Main(string[] args)
{
MongoCRUD db = new MongoCRUD("PartsManagerDB");
PartModel newPart = new PartModel()
{
PartName = "Lower Bracket",
PartNumber = "4000"
};
db.InsertPart<PartModel>("Projects", new Guid("f3784ba4-c422-43e0-80fd-41bb87b20f10"), newPart);
}
}
The project 93100 is already in my db, but after executing my code, no error comes up but the collection is not being updated as you can see below:

You are missing the line that actually executes the two things you already have, filter and update.
public void InsertPart<T>(string table, Guid id, PartModel newPart)
{
var collection = db.GetCollection<T>(table);
var filter = Builders<T>.Filter.Eq("Id", id);
var update = Builders<ProjectModel>.Update.Push<PartModel>(e => e.Parts, newPart);
// This line will actually update your DB with the new value.
collection.FindOneAndUpdate<T>(filter, update);
}
Documentation on FindOneAndUpdate() method.
Also, in your Main() method, you need to use the class, ProjectModel instead of PartModel when calling the InsertPart method.
db.InsertPart<**ProjectModel**>("Projects", new Guid("f3784ba4-c422-43e0-80fd-41bb87b20f10"), newPart);
In the InsertPart method, T is used in your filter's Builder and it should be ProjectModel to get the Project that you are updating... not Part. Part is being inserted to the ProjectModel's PartModel array so the T should be ProjectModel.

Related

MongoDb Driver autogenerate _id for BsonDocument

I'm trying to create several documents within a transaction, and one of the documents needs to be inserted as a BsonDocument as I have to be able to mutate a couple of values prior to insertion.
The two documents that I don't need to map .ToBsonDocument() are properly getting a new ID generated, but the main document is not.
Here is a basic class structure that I'm using.
public abstract class BaseDto
{
[JsonProperty("_id")] public string Id { get; init; }
[JsonProperty("_partitionKey")] public string PartitionKey { get; init; } = Constants.MasterDataPartitionKey;
}
public class ProductDto :BaseDto
{
public string Name { get; init; }
public string BrandId { get; init; }
public BrandDto Brand { get; init; }
public string ProducerId { get; init; }
public ProducerDto Producer { get; init; }
}
public class BrandDto :BaseDto
{
public string Name { get; init; }
}
public class ProducerDto :BaseDto
{
public string Name { get; init; }
}
And I'm configuring my mappings without the MongoDb specific attributes.
BsonClassMap.RegisterClassMap<BaseDto>(map =>
{
map.MapIdProperty(c => c.Id)
.SetElementName("_id")
.SetIdGenerator(CannectIdGenerator.Instance)
.SetSerializer(new StringSerializer(BsonType.String));
map.MapProperty(x => x.PartitionKey).SetElementName("_partitionKey");
});
BsonClassMap.RegisterClassMap<ProducerDto>(map => map.AutoMap());
BsonClassMap.RegisterClassMap<BrandDto>(map => map.AutoMap());
BsonClassMap.RegisterClassMap<ProductDto>(map => map.AutoMap());
The trouble I'm running into is that my Product isn't getting an auto-generated ID (the others are). Here's my transaction
[HttpPost]
public async Task<ActionResult<string>> Add(ProductDto product)
{
using var session = await _database.Client.StartSessionAsync();
try
{
session.StartTransaction();
// NOTICE THIS IS A `BsonDocument`
var products = _database.GetCollection<BsonDocument>(MasterCollection.Products.DisplayName);
var brands = _database.GetCollection<BrandDto>(MasterCollection.Brands.DisplayName);
var producers = _database.GetCollection<ProducerDto>(MasterCollection.Producers.DisplayName);
// THIS INSERT GETS A PROPER ID
await producers.InsertOneAsync(session, product.Producer);
// THIS INSERT GETS A PROPER ID
await brands.InsertOneAsync(session, product.Brand);
var insert = product.ToBsonDocument();
insert[nameof(ProductDto.Brand)] = insert[nameof(ProductDto.BrandId)] = product.Brand.Id;
insert[nameof(ProductDto.Producer)] = insert[nameof(ProductDto.ProducerId)] = product.Producer.Id;
// THIS INSERT DOES NOT GET AN ID, INSTEAD IT JUST STAYS `null`
await products.InsertOneAsync(session, insert);
await session.CommitTransactionAsync();
return Ok(insert["_id"]);
}
catch
{
await session.AbortTransactionAsync();
return Problem("Transaction Aborted");
}
}
What I'm trying to avoid is to have to manually initialize the ID field, I'd much prefer it to happen as a part of an insert.

DynamoDB - How to implement Optimistic Locking using ServiceStack.Aws

Currently, I am using ServiceStack.Aws v5.9.0 to communicate with DynamoDB. I have used PutItem for both creating and updating an item without anticipating data loss in case of concurrency handling.
public class Customer
{
[HashKey]
public int CustomerId { get; set; }
[AutoIncrement]
public int SubId { get; set; }
public string CustomerType { get; set; }
public string LastName { get; set; }
public string FirstName { get; set; }
...//and hundreds of fields here
}
public class CustomerDynamo
{
private readonly IPocoDynamo db;
//Constructor
public CustomerDynamo()
{
var dynamoClient = new AmazonDynamoDBClient(_region);
var entityType = typeof(Customer);
var tableName = entityType.Name;
entityType.AddAttributes(new AliasAttribute(name: tableName));
db = new PocoDynamo(dynamoClient) { ConsistentRead = true }.RegisterTable(tableType: entityType);
}
public Customer Update(Customer customer)
{
customer.ModifiedDate = DateTime.UtcNow;
db.PutItem(customer);
return customer;
}
}
The above Update method is called in every service/async task that needs to update the data of the customer.
Refer to this article of AWS I decided to implement the Optimistic Locking to save my life from the issue of concurrency requests.
https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBContext.VersionSupport.html
Assume that the VersionNumber will be the key for Optimistic Locking. So I added the VersionNumber into the Customer model.
public class Customer
{
[HashKey]
public int CustomerId { get; set; }
[AutoIncrement]
public int SubId { get; set; }
public string CustomerType { get; set; }
public string LastName { get; set; }
public string FirstName { get; set; }
...//and hundreds of fields here
[DynamoDBVersion]
public int? VersionNumber { get; set; }
}
The result is VersionNumber not updated while it should be automatically incremented. I think it is just because the PutItem will override the whole existing item. Is this correct?
I think I need to change from PutItem to UpdateItem in the Update method. The question is how can I generate the expression dynamically to be used with the UpdateItem?
Thanks in advance for any help!
Updates:
Thanks #mythz for the useful information about DynamoDBVersion attribute. Then I tried to remove the DynamoDBVersion and using the UpdateExpression of PocoDynamo as below
public Customer Update(Customer customer)
{
customer.ModifiedDate = DateTime.UtcNow;
var expression = db.UpdateExpression<Customer>(customer.CustomerId).Set(() => customer);
expression.ExpressionAttributeNames = new Dictionary<string, string>()
{
{ "#Version", "VersionNumber" }
};
expression.ExpressionAttributeValues = new Dictionary<string, AttributeValue>()
{
{ ":incr", new AttributeValue { N = "1" } },
{ ":zero", new AttributeValue { N = "0" } }
};
expression.UpdateExpression = "SET #Version = if_not_exists(#Version, :zero) + :incr";
if (customer.VersionNumber.HasValue)
{
expression.Condition(c => c.VersionNumber == customer.VersionNumber);
}
var success = db.UpdateItem(expression);
}
But the changes are not saved except the VersionNumber
The [DynamoDBVersion] is an AWS Object Persistence Model attribute for usage with AWS's DynamoDBContext not for PocoDynamo. i.e. the only [DynamoDB*] attributes PocoDynamo utilizes are [DynamoDBHashKey] and [DynamoDBRangeKey] all other [DynamoDB*] attributes are intended for AWS's Object Persistence Model libraries.
When needed you can access AWS's IAmazonDynamoDB with:
var db = new PocoDynamo(awsDb);
var awsDb = db.DynamoDb;
Here are docs on PocoDynamo's UpdateItem APIs that may be relevant.

How to query LIST by passing parameter?

I am using Entity Framework 5.0 and I created my database from model. The below is the screenshot of the edmx diagram.
I am working towards to a below structure of data:
On given Client ID give me list of Theader which belongs to that ClientID and its TReports so I modeled my models as below:
public class TReportHeaderModel
{
public int ID { get; set; }
public int ClientID { get; set; }
public string THeaderTitle { get; set; }
public int RowNumber { get; set; }
public IList<TReportModel> TReports { get; set; }
}
public class TReportModel
{
public int ID { get; set; }
public string TReportName { get; set; }
public string URL { get; set; }
public int RowNumber { get; set; }
}
So when I query to get Theaders and its each report for given clientID:
I am listing the headers first for given clientID:
public IList<TReportHeaderModel> GetHeadersByClient(int ClientID)
{
using (var connection = new TReportEntitiesConnection())
{
var clientHeaders= (from st in connection.THeaders
where ClientID == st.ClientID
select new TReportHeaderModel
{
ID=st.ID,
THeaderTitle=st.THeaderTitle,
RowNumber=st.RowNumber
}).ToList();
return (clientHeaders);
}
}
And then to get the list of reports for each title and this is where I am stuck--->
public IList<TReportModel> GetChildReportsByHeader(int THeaderID)
{
using (var connection = new TReportEntitiesConnection())
{
// ....
}
}
Instead of separating it by get the headers by client first and then get the report by header id, is it possible to combine it in one method? sorry for the confusing explanation but I am new to LINQ Query so please understand.
The below is the ideal structure for the UI implemetation:
Client ID =2
Header 1
TReportName
URL
Header 2
TReportName
URL
is it possible to combine it in one method?
If I understand you correctly, this is what you're looking for:
using (var connection = new TReportEntitiesConnection())
{
var clientHeaders = (
from st in connection.THeaders
where ClientID == st.ClientID
select new TReportHeaderModel
{
ID=st.ID,
THeaderTitle = st.THeaderTitle,
RowNumber = st.RowNumber,
Reports = from r in st.TReports
select new TReportModel
{
ID = r.ID,
TReportName = r.TReportName,
URL = r.URL,
RowNumber = r.RowNumber,
}
}
).ToList();
}
return clientHeaders;
Note that for this to work, TReportHeaderModel.TReports should be IEnumerable<TReportModel>.
Normally I would suggest you separate the methods for getting your data and transforming your data into DTOs like this (And usually I have the connection defined at the class level, not at the method level because I will reuse the connection many times, and I prefer keeping my data accesses as lazy as possible):
TReportEntitiesConnection conn = new TReportEntitiesConnection();
Then I will create extension methods like so:
public static class MyExtensions
{
public IQueryable<THeader> ByClientId(this IQuerable<THeader> conn, int ClientID)
{
return conn
.Include(h=>h.Reports)
.Where(h=>h.ClientID==ClientID);
}
public TReportHeaderModel ToDto(this THeader t)
{
return new TReportHeaderModel
{
ID=t.ID,
ClientID=t.ClientID,
THeaderTitle=t.THeaderTitle,
RowNumber=t.RowNumber,
Reports=t.Reports.ToDto()
};
}
public TReportModel ToDto(this TReport r)
{
return new TReportModel
{
ID=r.ID,
TReportName=r.TReportName,
URL=r.URL,
RowNumber=r.RowNumber
};
}
public IEnumerable<TReportHeaderModel> ToDto(this IEnumerable<THeader> h)
{
return h.Select(x=>x.ToDto());
}
public IEnumerable<TReportModel> ToDto(this IEnumerable<TReport> r)
{
return r.Select(x=>x.ToDto());
}
}
Then you can use it like so:
var result=conn.THeaders.ByClientId(200).ToDto();
If you prefer not having your connection at the module level, that is easy too:
using(var connection = new TReportEntitiesConnection())
{
var result=connection.THeaders.ByClientId(200).ToDto();
}
(or use AutoMapper and skip all the manual Dto conversions)

Locating a sub-object in MongoDB collection using LINQ/C#

I am attempting to write a code that finds objects in MongoDB collection with Linq.
Here's my code:
class Program
{
static void Main(string[] args)
{
var client = new MongoClient();
var db = client.GetDatabase("SoundsDB");
var collection = db.GetCollection<Sound>("SoundsCollection");
string myID = "0vvyXSoSHI";
var myObjects = collection
.Find(b => b.objectId == myID);
}
}
public class Sound
{
public string _id { get; set; }
public Result[] results { get; set; }
}
public class Result
{
public Audio_File audio_file { get; set; }
public DateTime createdAt { get; set; }
public string location { get; set; }
public string objectId { get; set; }
public DateTime updatedAt { get; set; }
}
public class Audio_File
{
public string __type { get; set; }
public string name { get; set; }
public string url { get; set; }
}
Here's the JSON in my MongoDB collection:
{
"_id" : ObjectId("56acced71b8ac8702446e8c6"),
"results" : [
{
"audio_file" : {
"__type" : "File",
"name" : "tfss-3c489351-0338-4903-8d94-a0f0c7091ef9-hi.m4a",
"url" : "http://files.parsetfss.com/hithere.m4a"
},
"createdAt" : "2014-12-27T22:59:04.349Z",
"location" : "Home",
"objectId" : "0vvyXSoSHI",
"updatedAt" : "2015-02-26T22:48:02.264Z"
}
]
}
I am trying to make it work but in the following line:
.Find(b => b.objectId == myID)
I get this error:
Cannot convert lambda expression to type 'MongoDB.Driver.FilterDefinition' because it is not a delegate type
Any idea how can I fix it and be able to search through the JSON for objects using their objectId?
Thanks!
I think the problem is that you are searching for a sub-document, not the main doc. Try this:
var myObjects = collection
.Find(b => b.results.Any(r=>r.objectId == myID));
Also - make sure that the objectId value is actually a string in your collection. It seems like it's a string in the object model but an objectId in the db. You may need to (a) change your object model and (b) change that query so that you are asking for r.objectId == ObjectId.Parse(myID) instead of the way I wrote it.
c# MongoDb .Find is Async
If you're using the c# drivers, you probably also need to implement async for this call:
static void Main() {
MainAsync().Wait();
}
static async Task MainAsync() {
var client = new MongoClient();
var db = client.GetDatabase("SoundsDB");
var collection = db.GetCollection<Sound>("SoundsCollection");
string myID = "0vvyXSoSHI";
var myObjects = await collection
.Find(b => b.objectId == myID).ToListAsync();
}
This way, you are using find, and converting the results to a list (so, myObjects will be a List<SoundsCollection> object).

MongoDB Unable to determine the serialization information for the expression error

My data is having following structure
public enum ParamType
{
Integer=1,
String=2,
Boolean=3,
Double=4
}
public class Gateway
{
public int _id { get; set; }
public string SerialNumber { get; set; }
public List<Device> Devices { get; set; }
}
public class Device
{
public string DeviceName { get; set; }
public List<Parameter> Parameters { get; set; }
}
public class Parameter
{
public string ParamName { get; set; }
public ParamType ParamType { get; set; }
public string Value { get; set; }
}
I filled 10 document objects of Gateway in a MongoDB database.
Now I want to query all those gateways which contains a device having Parameter with ParamName as "Target Temperature" and whose Value > 15.
I created following queries
var parameterQuery = Query.And(Query<Parameter>.EQ(p => p.ParamName, "Target Temperature"), Query<Parameter>.GT(p => int.Parse(p.Value), 15));
var deviceQuery = Query<Device>.ElemMatch(d => d.Parameters, builder => parameterQuery);
var finalQuery = Query<Gateway>.ElemMatch(g => g.Devices, builder => deviceQuery);
But when I run this, it is giving an exception
Unable to determine the serialization information for the expression: (Parameter p) => Int32.Parse(p.Value)
Please suggest where I am wrong.
As the error suggests, you can't use Int32.Parse inside your query. This lambda expression is used to get out the name of the property and it doesn't understand what Int32.Parse is.
If you are querying a string, you need to use a string value for comparison:
var parameterQuery = Query.And(Query<Parameter>.EQ(p => p.ParamName, "Target Temperature"), Query<Parameter>.GT(p => p.Value, "15"));
However, that's probably not what you want to do since you're using GT. To be treated as a number for this comparison you need the value to actually be an int in mongo, so you would need to change the type of your property:
public class Parameter
{
public string ParamName { get; set; }
public ParamType ParamType { get; set; }
public int Value { get; set; }
}
var parameterQuery = Query.And(Query<Parameter>.EQ(p => p.ParamName, "Target Temperature"), Query<Parameter>.GT(p => p.Value, 15));
Summary
I ran into this when I was modifying a list. It appears that Linq First/FirstOrDefault was not handled well by MongoDB for me. I changed to be an array index var update = Builders<Movie>.Update.Set(movie => movie.Movies[0].MovieName, "Star Wars: A New Hope"); Note: This is in Asp.Net 5 using MongoDB.Driver 2.2.0.
Full Example
public static void TypedUpdateExample() {
var client = new MongoClient("mongodb://localhost:27017");
var database = client.GetDatabase("test");
var collection = database.GetCollection<Movie>("samples");
//Create some sample data
var movies = new Movie {
Name = "TJ",
Movies = new List<MovieData>
{
new MovieData {
MovieName = "Star Wars: The force awakens"
}
}
};
collection.InsertOne(movies);
//create a filter to retreive the sample data
var filter = Builders<Movie>.Filter.Eq("_id", movies.Id);
//var update = Builders<Movie>.Update.Set("name", "A Different Name");
//TODO:LP:TSTUDE:Check for empty movies
var update = Builders<Movie>.Update.Set(movie => movie.Movies[0].MovieName, "Star Wars: A New Hope");
collection.UpdateOne(filter, update);
}
public class Movie {
[BsonId]
public ObjectId Id { get; set; }
public string Name { get; set; }
public List<MovieData> Movies { get; set; }
}
public class MovieData {
[BsonId]
public ObjectId Id { get; set; }
public string MovieName { get; set; }
}

Categories