While trying to Upsert a list # MongoDB i get the following error:
"Unable to determine the serialization information for o => o."
Order.cs *(removed properties for testing):
public class Order
{
[BsonId]
[BsonRepresentation(BsonType.String)]
public int Id { get; set; }
public string Number { get; set; }
}
Upsert:
public async Task SyncAsync(List<Order> orders)
{
var bulkUpdateModel = new List<WriteModel<Order>>();
foreach (var order in orders)
{
var filter = Builders<Order>.Filter.Eq(o => o.Id, order.Id);
var updateDefinition = Builders<Order>.Update.Set(o => o, order);
var upsertOne = new UpdateOneModel<Order>(filter, updateDefinition) { IsUpsert = true };
bulkUpdateModel.Add(upsertOne);
}
await _orders.BulkWriteAsync(bulkUpdateModel);
}
You could try the ReplaceOneModel instead:
var upsertOne = new ReplaceOneModel<Order>(filter, order)
{
IsUpsert = true
};
Solution
I found out that you can't update a whole document, only certain properties you set in the updateDefinition...
Not working:
var updateDefinition = Builders<Order>.Update.Set(o => o, order);
Working:
var updateDefinition = Builders<Order>.Update.Set(o => o.Whatever, order.Whatever);
I don't want to use reflection, so switched to Replace where it's possible to update the whole document.
Example
public async Task SyncAsync(List<Order> orders)
{
var bulkUpdateModel = new List<WriteModel<Order>>();
foreach (var order in orders)
{
var filter = Builders<Order>.Filter.Eq(o => o, order);
var writeModel = new ReplaceOneModel<Order>(filter, order) { IsUpsert = true };
bulkUpdateModel.Add(writeModel);
}
await _orders.BulkWriteAsync(bulkUpdateModel);
}
Related
I have a HttpResponseMessage method that returns a JSON based on DB data:
public HttpResponseMessage RespMsg(JObject jsonData)
{
HttpResponseMessage response = new HttpResponseMessage();
dynamic json = jsonData;
int recId = jsonData.Id;
var respStructure = myTable.Where(r => r.Id==recId).Select(t => new
{
t.Id,
t.Name
}
var responseJson = JsonConvert.SerializeObject(respStructure);
response.Content = new StringContent(responseJson, null, "application/json");
return response;
}
The response I get is something like {"Id":3,"Name":Third}.
The row in T1, has multiple rows in table T2
var t2Resp = T2.Where(c => c.T1Id == recId);
foreach (var t in t2Resp) {
//call a method that return an object with computed data
}
Is there a way to add the data from foreach as separate JSON like {"Id":3,"Name":"Third"} {first data from foreach} {second data from foreach}? The first one is from T1 query and the next ones depending on t2Resp length
Hint:- First You have to create a DTO object that matches with your response. Since T1 and T2 have one-to-many relationship create a DTO class with below structure.
public class DtoData
{
public string id { get; set; }
public string Name { get; set; }
public List<DaatFromT2> T2Data { get; set; }
}
public class DaatFromT2
{
public string prop1 { get; set; }
public int prop2 { get; set; }
public DateOnly prop3 { get; set; }
}
Then you have to use this class to populate data from T1 and T2 and finally sterilize to JSON. Something as shown below .
var resposeStructure = new List<DtoData>();
var t1data = myTable.Where(r => r.Id==recId).Select(t => new
{
t.Id,
t.Name
};
var t2Resp = T2.Where(c => c.T2Id == recId);
foreach (var t in t1data)
{
var data = new DtoData
{
id = t.Id,
Name = t.Name,
T2Data = t2Resp.Where(c => t.id == t2Resp.someid)
.Select(t => new
{
//call a method that return an object with computed data
//and map to corresponding properties
});
}
resposeStructure.Append(data);
}
var responseJson = JsonConvert.SerializeObject(respStructure);
May be theses code snippets give you some idea and able to sort this issue.
Happy coding :)
I tried to delete dublicate records using MongoDb C# Driver. The script below neither raise an error nor delete any record. Any advice ?
[BsonIgnoreExtraElements]
public class ApiUsers
{
[BsonId]
[BsonRepresentation(BsonType.String)]
public ObjectId Id { get; set; }
public string UserEMail { get; set; }
}
var users = _mongoDbContext.ApiUsers.Find(f => f.UserEMail == email).ToList();
var oneUser = users[0];
var idsToDelete = users.Where(x => !x.Id.Equals(oneUser.Id)).Select(x => x.Id);
if (idsToDelete.Any())
{ //Delete Dublicates
var idsToDeleteFilter = Builders<ApiUsers>.Filter.In(t => t.Id, idsToDelete);
var result = _mongoDbContext.ApiUsers.DeleteMany(idsToDeleteFilter);
}
I tried also DeleteOne method like below, but it didn't work either,
foreach (var idToDelete in idsToDelete)
_mongoDbContext.ApiChildUsers.DeleteOne(Builders<ApiChildUsers>.Filter.Eq(u => u.Id, idToDelete));
you could try the following approach where you get back an Id to keep and delete the rest like so:
var filter = Builders<ApiUsers>.Filter.Where(u => u.UserEMail == "test#test.com");
var projection = Builders<ApiUsers>.Projection.Expression(u=>u.Id);
var options = new FindOptions<ApiUsers, string> { Projection = projection, Limit = 1 };
var idToKeep = collection.FindSync(filter, options).ToList()[0];
collection.DeleteMany(
u => u.UserEMail == "test#test.com" &&
u.Id != idToKeep);
as a sidenote, i'd like to suggest you store the IDs in the server as ObjectId instead of string because it's less efficient as ObjectId only takes 12 bytes to store which would be less than the hexadecimal string representation of it.
here's a less verbose test program using mongodb.entities:
using MongoDB.Entities;
using System.Threading.Tasks;
namespace TestApp
{
public class ApiUsers : Entity
{
public string UserEMail { get; set; }
}
internal static class Program
{
private static async Task Main()
{
await DB.InitAsync("test", "localhost");
await new[] {
new ApiUsers { UserEMail = "test#test.com"},
new ApiUsers { UserEMail = "test#test.com"},
new ApiUsers { UserEMail = "test#test.com"},
new ApiUsers { UserEMail = "teX#teX.com"},
}.SaveAsync();
var idToKeep = (await DB.Find<ApiUsers, string>()
.Match(u => u.UserEMail == "test#test.com")
.Project(u => u.ID)
.Limit(1)
.ExecuteAsync())[0];
await DB.DeleteAsync<ApiUsers>(
u => u.UserEMail == "test#test.com" &&
u.ID != idToKeep);
}
}
}
how to retrieve partial object?
{
Id:123,
Name:"david",
Languages:[{b:"en"},{b:"ru"}]
}
public async Task<myObj> Get(long id, string lang=null)
{
FilterDefinition<myObj> filter = Builders<myObj>.Filter.Eq(s => s.Id, id)
& Builders<myObj>.Filter.ElemMatch(l => l.Languages, s => s.b== lang);
ProjectionDefinition<myObj> projection = Builders<Symptom>.Projection
.Include(d => d.Id)
.Include(d => d.Name)
.Include(d => d.Languages[-1]);
FindOptions<myObj> options = new FindOptions<myObj> { Projection = projection };
using (IAsyncCursor<myObj> cursor = await db.Collection.FindAsync(filter, options))
{
return cursor.SingleOrDefault();
}
}
if i call function get(123,"cn") i expect to get:
{
Id:123,
Name:"david",
Languages:null
}
instead of null.
how to fix the query to achieve my demand?
i think this will get the job done:
public async Task<myObj> Get(long id, string lang = null)
{
var res = await db.Collection.AsQueryable()
.Where(m =>
m.Id == id &&
m.Languages.Any(l => l.b == lang))
.SingleOrDefaultAsync();
return res ?? new myObj { _Id = id, Languages = null };
}
If you want to display the languages only when they match (and null if none match up), then try the following
public async Task<myObj> Get(long id, string lang = null)
{
FilterDefinition<myObj> filter = Builders<myObj>.Filter.Eq(s => s.Id, id)
var result = await collection.Find(filter).SingleOrDefaultAsync();
if (result != null)
result.Languages = result.Languages?.Where(lng => lng.b.Equals(lang)).ToList();
return result;
}
You will get your object that you want based on the ID.. then further it will return only those languages that match up with language that you are passing (null or otherwise).
It's working. I don't know what you mean by "instead of null".
One minor thing, that you would like to not include Languges, instead you projected the Languages to an array range with the [-1]. So it's just return last element of the array. The final code is:
> db.ItemWithLanguages.find()
{ "_id" : ObjectId("5dfb57c9692d22eefa6e0cfe"), "Id" : 123, "Name" : "david", "Languages" : [ { "B" : "en" }, { "B" : "cn" } ] }
internal class MyObj
{
public long Id { get; set; }
[BsonId]
[BsonElement("_id")]
public ObjectId MyId { get; set; }
public string Name { get; set; }
public List<Language> Languages { get; set; }
}
internal class Language
{
public string B { get; set; }
}
public static async Task<MyObj> Get(IMongoCollection<MyObj> collection, long id, string lang = null)
{
FilterDefinition<MyObj> filter = Builders<MyObj>.Filter.Eq(s => s.Id, id)
& Builders<MyObj>.Filter.ElemMatch(l => l.Languages, s => s.B == lang);
// excluding d.Languages by not including it.
// it makes Languages = null.
ProjectionDefinition<MyObj> projection = Builders<MyObj>.Projection
.Include(d => d.Id)
.Include(d => d.Name);
FindOptions<MyObj> options = new FindOptions<MyObj> { Projection = projection };
using (IAsyncCursor<MyObj> cursor = await collection.FindAsync(filter, options))
{
return cursor.SingleOrDefault();
}
}
...
string connectionString = "mongodb://localhost:27017";
var client = new MongoClient(connectionString);
var db = client.GetDatabase("test");
var myObjs = db.GetCollection<MyObj>("ItemWithLanguages");
MyObj ret;
Task.Run(async () => { ret = await Get(myObjs, 123, "cn"); }).ConfigureAwait(false).GetAwaiter()
.GetResult();
So I'm not sure if this is possible, but I'm doing a final select query, and I've noticed that it would be easier for my data if I could do something like:
var finalQuery = selectQuery
.GroupBy(x => x.ProductName)
.Select(c => new
{
c.Key = c.Count()
}).ToList();
Therefore, I want the returned data to be something like:
[{
"Clothes" : 5,
"Shoes" : 7,
"Laptop" : 10
}]
My current query is:
var finalQuery = selectQuery
.GroupBy(x => x.ProductName)
.Select(c => new
{
ProductName = c.Key
ProductCount = c.Count()
}).ToList();
I thought something like c.Key.ToString() would do the trick, but it doesn't work. I'm guessing the "Key" must be a set value to work, and it can't necessarily be dynamically changed?
You can try this way
var finalQuery = selectQuery.GroupBy(p => p.ProductName).ToDictionary(g => g.Key, g => g.Count());
You should customize the string using your own code, or you can use an external library as NewtonSoft.Json.
Below is a sample code that does the trick for you.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
public class Test
{
public static void Main()
{
var products = new List<Product>()
{
new Product("a", "Clothes"),
new Product("b", "Shoes"),
new Product("c", "Clothes"),
new Product("d", "Clothes"),
new Product("e", "Shoes"),
new Product("f", "Shoes"),
new Product("g", "Laptop"),
new Product("h", "Laptop"),
new Product("h", "Shoes"),
};
var result = products
.GroupBy(p => p.Type)
.Select(
group => new
ProductCount(group.Key, group.Count())
// You can use also an object as the commented code below
//{
// Type = group.Key,
// Count = group.Count()
//}
)
.ToList();
Console.WriteLine(ProductCount.ToFormatedString(result));
}
class Product
{
public string Name { get; set; }
public string Type { get; set; }
public Product(string name, string type)
{
this.Name = name;
this.Type = type;
}
}
class ProductCount
{
public string Type { get; set; }
public int Count { get; set; }
public ProductCount(string type, int count)
{
this.Type = type;
this.Count = count;
}
public override string ToString()
{
return $"\"{this.Type}\" : {this.Count}";
}
public static string ToFormatedString(IEnumerable<ProductCount> products) // if you need a more generic method, u can make this an extension method and use Reflection
// Or u can use a nuget package that formats objects to json (e.g: Newtonsoft is a good library)
{
var sb = new StringBuilder();
sb.AppendLine("[{");
foreach (var item in products)
sb.AppendLine(item.ToString());
sb.AppendLine("}]");
return sb.ToString();
}
}
}
The code generates all events of a specific producer, so that works.
But the problem is that when I return new ApiListModel< EventPerYearReport >() it goes to the class ApiListModel and then it says "status_code": 200, "data": null.
How do I generate, by executing the following URL: /api/dashboard/1, a value instead of "status_code": 200, "data": null?
DashboardController.cs
[HttpGet("{id}")]
public async Task<ApiListModel<EventPerYearReport>> GetEventPerYearReport(int id)
{
return await _dashboardService.GetEventPerYearReport(id);
}
DashboardService.cs
public async Task<ApiListModel<EventPerYearReport>> GetEventPerYearReport(int id)
{
var results = _dbContext.Events
.Where(p => p.ProducerId == id)
.GroupBy(e => e.DateStartEvent.ToString("yyyy"), (year, values) => new EventPerYear
{
year = year,
total = values.Count(),
}).ToList();
var report = new EventPerYearReport()
{
eventsPerYear = results
};
return new ApiListModel<EventPerYearReport>();
}
ApiListModel.cs
public class ApiListModel<T>
{
[JsonProperty(PropertyName = "status_code")]
public int StatusCode = 200;
[JsonProperty(PropertyName = "data")]
public List<T> Data;
}
EventPerYearReport.cs
public class EventPerYearReport
{
public List<EventPerYear> eventsPerYear { get; set; }
}
Your DashboardService.cs is returning new ApiListModel<EventPerYearReport>(); this will always have null data.
To fix this you need to use the following
public async Task<ApiListModel<EventPerYearReport>> GetEventPerYearReport(int id)
{
var results = _dbContext.Events
.Where(p => p.ProducerId == id)
.GroupBy(e => e.DateStartEvent.ToString("yyyy"), (year, values)
=> new EventPerYear
{
year = year,
total = values.Count(),
}).ToList();
return new ApiListModel<EventPerYearReport>()
{
Data = new EventPerYearRaport()
{
eventsPerYear = results
}
};
}