Mongo C# Driver how to do nested ElemMatch - c#

If I have following architecture - how do I find zoos with noisy animals?
Example Data Model
public class Zoo
{
public List<Cage> Cages { get; set; }
}
public class Cage
{
public List<Animal> Animals { get; set; }
}
public abstract class Animal
{
public string Name { get; set; }
}
public class Feline : Animal
{
public int MeowsCount { get; set; }
}
public class Canine: Animal
{
public int BarksCount { get; set; }
}
For now I do extract whole db and find necessary with LINQ
public IEnumerable<Zoo> FindNoisyZoos(int soundsCount)
{
var allZoos = await zooCollection.Find(_ => true).ToListAsync();
List<Zoo> noisyZoos = allZoos.Where(z =>
z.Cages.Any(c =>
c.Animals.Where(a => ((Feline)a).MeowsCount == soundsCount || ((Canine)a).BarksCount == soundsCount))))
.ToList();
return noisyZoos;
}
And thats terribly inefficient. I would like being able to do nested ElemMatch, but I can't wrap my head around how ElemMatch works. I expect it to be something like this:
public IEnumerable<Zoo> FindNoisyZoos(int soundsCount)
{
var noisyFelineFilter = new FilterDefinitionBuilder<Animal>().And(new FilterDefinitionBuilder<Animal>().OfType<Feline>(), new FilterDefinitionBuilder<Animal>().Eq(a => ((Feline)a).MeowsCount == soundsCount));
var noisyCanineFilter = new FilterDefinitionBuilder<Animal>().And(new FilterDefinitionBuilder<Animal>().OfType<Canine>(), new FilterDefinitionBuilder<Animal>().Eq(a => ((Canine)a).BarksCount == soundsCount));
var noisyAnimalFilter = new FilterDefinitionBuilder<Animal>().Or(noisyFelineFilter, noisyCanineFilter);
// smth like this: new FilterDefinitionBuilder<Zoo>().ElemMatch(z => z.Cages.ElemMatch(c => c.Animals.ElemMatch(noisyAnimalFilter)));
var noisyFilter;
var zoos = await zooCollection.Find(noisyFilter).ToListAsync();
return zoos;
}

MongoDB .NET driver can process some LINQ queries server-side. It translates them under the hood into MongoDB native ones. Did you try something like zooCollection.AsQueriable().Where(z => z...).ToList(). .ToList() comes last and sends data to the client. If this works, the filtering will be performed server-side, if it doesn't the driver will tell you directly that it is not supported.

Related

How can I read just one field in MongoDB C#?

Im trying to read just one field from the MongoDB with a given ID, but its giving me all the object... How can I do it ? What should I do? I tried with this repository but its returning me all the object
Here is my class:
public class Hall
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; }
public string Name { get; set; }
public List<string> Surveys { get; set; }
}
And here is my repository with the connection to Mongo:
public Task<List<Hall>> ReadSurveysHall(string hallId)
{
var filter = Builders<Hall>.Filter.(x => x.Id, hallId);
return _mongoManager.Find(filter, MongoConstants.HallCollection);
}
I just want it to give me just the list of Survey's string.
Thank you in advance.
In order to return only the list of surveys, you can change your method like this:
public async Task<List<string>> ReadSurveysHall(string hallId)
{
var filter = Builders<Hall>.Filter.(x => x.Id, hallId);
var halls = await _mongoManager.Find(filter, MongoConstants.HallCollection);
var hall = halls.FirstOrDefault();
if (hall == null)
return new List<string>();
return hall.Surveys;
}
You can further optimize this by using a projection so that MongoDB only delivers the data that you are interested in, e.g.:
var projection = Builders<Hall>.Projection.Exclude(x => x.Id).Include(x => x.Surveys);
var options = new FindOptions<Hall, Hall>()
{
Projection = projection,
};
How you use that projection depends on the capabilities of _mongoManager. Take a look at the Find methods and check whether any of these accepts a projection or some kind of FindOptions as a parameter.

How to do aggregation sort in mongodb inner object with c#

I need to sort inner object sort of a class with mongo db. i need to sort based on marks
Sample Json:
{"_id":"58e46f81c4734559ac8082f0","Name":"test","Students":[{"Name":"A","Marks":"988"}]}
{"_id":"58e46f81c4734559ac8082f1","Name":"sahir","Students":[{"Name":"sahir","Marks":"311"}]}
Here is the class:
public class Student
{
[BsonId]
public ObjectId Id { get; set; }
[BsonElement]
public string Name { get; set; }
public ICollection <Students> Students { get; set; }
}
public class Students
{
public string Name { get; set; }
public string Marks{ get; set; }
}
Action
public IActionResult Index()
{
//Get the database connection
mongoDatabase = GetMongoDatabase();
var x = mongoDatabase.GetCollection<Student>("student").Find(FilterDefinition<Student>.Empty).ToList();
var _dbContext = mongoDatabase.GetCollection<Student>("student");
// mongo db document collection student class students objects need to sort based on marks.
_dbContext.Aggregate<Student>()
.Sort(x=> x.students.marks).Skip((page-1)*pageSize).Limit(100);
.ToList(); -- how to do that sort in this line
or
var result = _dbContext.Aggregate<Student>().Sort(new BsonDocument("Marks", 1)).ToList();
return View(result);
}
Note:
Don't provide linq Solution to convert asQuerable operators to do LINQ queries. This question is only based on an understanding of MongoDB aggregation sort and how to use that in C#.
No forum has any reference for internal objects sorting with c#.There are lots of examples of C# mongodb sorting without aggragtion
Problem:
I have aggregation sort mongo shell query .. don't know to implement that in c#
are you trying to get a list of all students sorted by their marks? if so, try this:
var students = await collection.AsQueryable()
.SelectMany(c => c.Students)
.OrderBy(s => s.Marks)
.Skip(50)
.Take(10)
.ToListAsync();
the easiest way to get the result you want is via the AsQueryable() interface.
if you must use the Aggregate() api, you can do the following:
var students = await collection.Aggregate()
.Unwind(c => c.Students)
.ReplaceWith<BsonDocument>("$Students")
.Sort("{Marks:1}")
.Skip(0)
.Limit(10)
.As<Student>()
.ToListAsync();
test program:
using MongoDB.Driver;
using MongoDB.Driver.Linq;
using MongoDB.Entities;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace TestApplication
{
public class Course : Entity
{
public IEnumerable<Student> Students { get; set; }
}
public class Student
{
public string Name { get; set; }
public string Marks { get; set; }
}
public static class Program
{
private static async Task Main()
{
await DB.InitAsync("test");
await new[] {
new Course
{
Students = new[] {
new Student { Name = "a", Marks = "100"},
new Student { Name = "b", Marks = "200"},
}
},
new Course
{
Students = new[] {
new Student { Name = "d", Marks = "400"},
new Student { Name = "c", Marks = "300"},
}
}
}.SaveAsync();
var students = await DB.Queryable<Course>()
.SelectMany(c => c.Students)
.OrderBy(s => s.Marks)
.Skip(50)
.Take(10)
.ToListAsync();
}
}
}

Lookup-able models

I'm currently working on a project using MongoDB as it's database and I got some issues on how to solve the problem of having a model containing some ids to foreign documents and looking them up without needing to fall back to BsonDocument.
Originally I had this (stripped down) model class:
public class TestSession
{
[BsonId]
public ObjectId Id { get; set; }
[BsonElement("taskId")]
public ObjectId? TaskId { get; set; }
}
Sometimes I need the concrete TaskConfiguration referenced by TaskId.
In this case I use a $lookup to resolve it.
First I introduced a new member to hold the concrete TaskConfiguration instance changing my model class to:
public class TestSession
{
[BsonId]
public ObjectId Id { get; set; }
[BsonElement("taskId")]
public ObjectId? TaskId { get; set; }
[BsonIgnore]
public TaskConfiguration Task { get; set; }
}
Then I realized that the aggregation was unable to deserialize the looked-up and unwound Task value, I guess because of BsonIgnore.
I could not find any options to override this and I couldn't come up with any other solution to this.
Finally I decided to split the model class into the initial one containing exactly the database representation of the document and a derived class containing the resolved Task member without the BsonIgnore attribute in order for it to be deserializable.
The resulting classes look like this:
public class TestSessionModel
{
[BsonId]
public ObjectId Id { get; set; }
[BsonElement("taskId")]
public ObjectId? TaskId { get; set; }
}
public class TestSession : TestSessionModel
{
[BsonElement("task")]
public TaskConfiguration Task { get; set; }
}
Although this approach seems to work I now have to split up all similiar classes which not only creates a lot of classes potentially but also could potentially confuse the reader what class to use.
Additionally one has to be careful to not store the derived classes into the database.
My question now is if my current approach is actually the way to go in this case or if there are better/safer alternatives?
you can do it with the BsonIgnore approach but you need to project the final result like this:
var res = collection.AsQueryable()
.Where(t => t.Id == session.Id)
.Join(
foreignColl.AsQueryable(), // foreign collection
s => s.TaskId, // local field
c => c.Id, // foreign field
(s, c) => new TestSession // projection
{
Id = s.Id,
Name = s.Name,
TaskId = s.TaskId,
Task = c
})
.Single();
here's the full program i used for testing:
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Entities;
using MongoDB.Entities.Core;
using System.Linq;
namespace StackOverflow
{
public class TestSession : Entity
{
public string Name { get; set; }
[BsonRepresentation(BsonType.ObjectId)]
public string TaskID { get; set; }
[BsonIgnore]
public TaskConfiguration Task { get; set; }
}
public class TaskConfiguration : Entity
{
public int NumOfIterations { get; set; }
}
public static class Program
{
private static void Main()
{
new DB("test");
var task = new TaskConfiguration { NumOfIterations = 10 };
task.Save();
var session = new TestSession { Name = "This is a test session", TaskID = task.ID };
session.Save();
var res = DB.Queryable<TestSession>()
.Where(t => t.ID == session.ID)
.Join(
DB.Queryable<TaskConfiguration>(),
s => s.TaskID,
c => c.ID,
(s, c) => new TestSession
{
ID = s.ID,
Name = s.Name,
TaskID = s.TaskID,
Task = c
})
.Single();
}
}
}

What is the best solution to move complex queries from controller into repository?

during creating my app I threw Model layer and Repository layer to separate projects. Each form in my app is represented by its own ViewModel class. All ViewModel classes are stored in folder ViewModels in MyApplicationName.Web. During GET request for MyDrafts form, the following function is launched:
public ActionResult MyDrafts()
{
MyDraftsVM dataVM = GetDataMyDrafts();
return View(dataVM);
}
private MyDraftsVM GetDataMyDrafts()
{
MyDraftsVM dataVM = new MyDraftsVM();
using (var context = new PrincipalContext(ContextType.Domain))
{
List<MyDraftsVM.MyDraftVM> userInvoices = new List<MyDraftsVM.MyDraftVM>();
userInvoices = _repoExhibitor.Context.Exhibitors
.Join(_repoExhibitor.Context.Invoices.Where(x => x.CreatedBy == User.Identity.Name && x.Status == (int)(ModelEnums.Invoice.Status.Przygotowanie)),
e => e.Id,
i => i.Id,
(e, i) => new { e, i })
.ToList()
.Select(s => new MyDraftsVM.MyDraftVM(s.e, s.i, UserPrincipal.FindByIdentity(context, s.i.CreatedBy).DisplayName))
.ToList();
List<MyDraftsVM.MyDraftVM> userCorrespondence = new List<MyDraftsVM.MyDraftVM>();
userCorrespondence = _repoExhibitor.Context.CorrespondenceSenders
.Join(_repoExhibitor.Context.Correspondences.Where(x => x.CreatedBy == User.Identity.Name && x.Status == (int)(ModelEnums.Invoice.Status.Przygotowanie)),
sen => sen.Id,
c => c.Id,
(sen, c) => new { sen, c })
.ToList()
.Select(s => new MyDraftsVM.MyDraftVM(s.c, UserPrincipal.FindByIdentity(context, s.c.CreatedBy).DisplayName))
.ToList();
dataVM.Documents.AddRange(userInvoices);
dataVM.Documents.AddRange(userCorrespondence);
}
return dataVM;
}
MyDraftsVM class looks like this:
public class MyDraftsVM
{
public MyDraftsVM()
{
this.Documents = new List<MyDraftVM>();
this.Layout = "~/Views/Shared/_LayoutBox.cshtml";
}
public List<MyDraftVM> Documents { get; set; }
/// <summary>
/// layout path
/// </summary>
public string Layout { get; set; }
public class MyDraftVM
{
public MyDraftVM()
{
this.DocumentPartial = new DocumentMemebership();
this.InvoicePartial = new InvoiceMembership();
this.ExhibitorPartial = new ExhibitorMembership();
this.CorrespondencePartial = new CorrespondenceMembership();
}
public MyDraftVM(Exhibitor e, Invoice i, string createdBy)
{
InvoicePartial = Mapper.Map<MyDraftsVM.MyDraftVM.InvoiceMembership>(i);
ExhibitorPartial = Mapper.Map<MyDraftsVM.MyDraftVM.ExhibitorMembership>(e);
DocumentPartial = new MyDraftsVM.MyDraftVM.DocumentMemebership()
{
DocumentType = "Invoice",
Number = i.InvoiceNumber,
IssuingDate = i.IssuingDate,
CreatedBy = createdBy
};
}
public MyDraftVM(Correspondence c, string createdBy)
{
CorrespondencePartial = Mapper.Map<MyDraftsVM.MyDraftVM.CorrespondenceMembership>(c);
DocumentPartial = new MyDraftsVM.MyDraftVM.DocumentMemebership()
{
DocumentType = "Correspondence",
Number = c.Signature,
IssuingDate = c.IssuingDate,
CreatedBy = createdBy,
};
}
public DocumentMemebership DocumentPartial { get; set; }
public InvoiceMembership InvoicePartial { get; set; }
public CorrespondenceMembership CorrespondencePartial { get; set; }
public ExhibitorMembership ExhibitorPartial { get; set; }
public class InvoiceMembership : InvoiceVM
{
public virtual int Id { get; set; }
}
public class CorrespondenceMembership : CorrespondenceVM
{
}
public class ExhibitorMembership : ExhibitorVM
{
}
public class DocumentMemebership : DocumentVM
{
}
}
}
I’d like to move complex queries with joins to repository and here appears the problem. I would call this problem “problem of the interaction projects” because project MyApplicationName .Web has reference MyApplicationName.Repository. If I would like to move queries to MyApplicationName.Repository, I have to move there also MyDraftsVM object, which belongs to MyApplicationName.Web.
The only solution which I see now, is to separate ViewModels folder as a separate project and the give a reference MyApplicationName.Web and MyApplicationName.Repository to this project. I don’t know if this is a good idea so that’s why I’m asking you.
If it’s not a good idea, can you give me better solution to move those complex queries to repository? Thank you.
You're on the right way, moving the queries in separate repo project. But MyApplicationName.Repository should not know about your web project neither about your view models. It should work only with your domain models. So the option that I would suggest you is to use some mapping tool between your domain and view models like AutoMapper.
This whould solve your architectural problems, I think.

C# Build where clause dynamically in Linq? for RavenDB

My code like bellow:
This Main class :
public class Product {
public string Id { set; get; }
public IList<Attr> Attributes { set; get; }
}
This child class of main class :
public class Attr
{
public string Key { set; get; }
public object Value { set; get; }
}
Filter item class:
public class Filter
{
public CompareType Type { set; get; }
public string Key { set; get; }
public object Value { set; get; }
}
Linq extension fuction for querying :
public static class LINQExtension
{
public static bool isMatch(this Product prod, this List<Filter> filters)
{
foreach(Filter F in filters){
Attr attribute = prod.Attributes.Any(A => A.Key == F.Key);
switch(F.Type){
case CompareType.CONTAIN: return ((string)attribute.Value).Contains(F.Value);
case ....
default: return false;
}
}
}
}
Filtering products result: (Not working)
public ActionResult FilterProducts(string word, decimal min, decimal max){
List<Filter> Conditions = new List<Filter> {
new Filter {Key = "Price", Type = CompareType.BETWEEN, Value = new decimal[] {min, max} },
new Filter {Key = "Title", Type = CompareType.CONTAIN, Value = word }
...
};
var Result = Session.Query<Product>().Where(P => P.isMatch(Conditions)).ToList();
return View(Result);
}
When it tried to run give errors like below:
{"Could not understand expression: .Where(P => P.isMatch(value(App.Controllers.HomeController+<>c__DisplayClass2).Conditions)).ToList()"}
In general, RavenDB's linq provider implementation is not equal to Linq-to-Objects provider.
Under the hood, Raven's client API serializes linq query experssion to Lucene query, then makes a REST call to server with that query. (You can use Fiddler to see it happen)
For example, given a database named Test with Northwind sample data and the query code (and assuming you have Fiddler active)
using (var store = new DocumentStore
{
Url = "http://localhost.fiddler:8080",
DefaultDatabase = "Test"
})
{
store.Initialize();
using (var session = store.OpenSession())
{
var result = session.Query<Order>().Where(x =>
x.Company == "companies/58" && x.Freight < 30m).ToList();
}
}
you will see the following REST call to the server (after url decoding)
http://localhost:8080/databases/Test/indexes/dynamic/Orders?&query=Company:companies/58 AND Freight_Range:{* TO Dx30}&pageSize=128&SortHint-Freight_Range=Double
What you see highlighted in the url is Linq query "serialized" into Lucene query.
In your case the error that you are seeing is simply Raven's linq implementation cannot understand how to transform your custom code into Lucene query

Categories

Resources