Getting a System.NotSupportedException when using MongoDB.Driver.Linq - c#

I am using MongoDB for my database and I am using ASP.NET Core for my api. I am trying to convert my Items object to a ItemsDTO (Data Transfer Object), because I don't want to return the Id. However, when I use
_items.AsQueryable()
.Where(item => item.Game.Contains("Games/1"))
.Select(item => ItemToDTO(item))
.ToList();
It gives back a System.NotSupportedException: 'ItemToDTO of type Project.Services.ItemService is not supported in the expression tree ItemToDTO({document}).'
By the way ItemToDTO looks like this:
private static ItemDTO ItemToDTO(Item item)
{
return new ItemDTO
{
Name = item.Name,
Type = item.Type,
Price = item.Price,
Game = item.Game,
CostToSell = item.CostToSell,
Description = item.Description
};
}
So I am confused on why this doesn't work because before I was using a SQL Server Database and normal Linq that looked like this:
_context.Items
.Where(item => item.Game.Contains("Games/1"))
.Select(item => ItemToDTO(item))
.ToList(),
and it worked the data that I wanted. I know that when using the MongoDB.Driver I am using their Linq and Queryable objects. Just wondering why I am getting the error above.

It is typical mistake when working with LINQ. Translator can not look into ItemToDTO function body and will materialize whole item and then call ItemToDTO to correct result. Probably it is not a way for MongoDb and bad way for relational databases.
So I propose rewrite your function and create extension:
public static IQueryable<ItemDTO> ItemToDTO(this IQueryable<Item> items)
{
return items.Select(item => new ItemDTO
{
Name = item.Name,
Type = item.Type,
Price = item.Price,
Game = item.Game,
CostToSell = item.CostToSell,
Description = item.Description
});
}
Then your query should work fine:
_context.Items
.Where(item => item.Game.Contains("Games/1"))
.ItemToDTO()
.ToList()

Related

Unable to cast object of type 'MongoDB.Bson.BsonString' to type 'MongoDB.Bson.BsonDocument' in MongoDB .NET Driver

I am facing a problem while trying to run an aggregation pipeline using MongoDB .NET client. My code looks like so:
public async Task<IEnumerable<string>> GetPopularTags(int count)
{
var events = _database.GetCollection<Event>(_eventsCollectionName);
var agg = events.Aggregate();
var unwind = agg.Unwind<Event, Event>(e => e.Tags);
var group = unwind.Group(e => e.Tags, v => new { Tag = v.Key, Count = v.Count() });
var sort = group.SortByDescending(e => e.Count);
var project = group.Project(r => r.Tag);
var limit = project.Limit(count);
var result = await limit.SingleOrDefaultAsync();
return result;
}
(separate vars for each stage are just for debugging purposes)
While trying to get the result of the pipeline (last var) I get a following error:
System.InvalidCastException: Unable to cast object of type 'MongoDB.Bson.BsonString' to type 'MongoDB.Bson.BsonDocument'
What am I missing?
Thanks in advance for any help!
SOLUTION
I finally figured out that the fact that I was getting an exception at the last line had nothing to do with where the error was. I tried running .SingleOrDefault() on every stage to see outputs and I noticed that my pipeline had a couple of issues.
My unwind stage was trying to return an Event object, but since it was unwinding Tags property (which was a List<string>), it was trying to set it to string and was throwing an exception. I solved that issue by letting it set an output type to the default of BsonDocument and then in next stage using ["Tags"] accessor to get the value I need. It looked something like this:
var dbResult = await events.Aggregate()
.Unwind(e => e.Tags)
.Group(e => e["Tags"], v => new { Tag = v.Key, Count = v.Count() })
My project stage was not working for some reason. I was not able to get the Tag property (which turned out to be a BsonValue type) to be converted to string. In the end I deleted that stage and replaced it with a dbResult.Select(t => t.Tag.AsString) to cast it to a string. Not the most elegant solution, but better than nothing.
In the end my code ended up looking like so:
public async Task<IEnumerable<string>> GetPopularTags(int count)
{
var events = _database.GetCollection<Event>(_eventsCollectionName);
var dbResult = await events.Aggregate()
.Unwind(e => e.Tags)
.Group(e => e["Tags"], v => new { Tag = v.Key, Count = v.Count() })
.SortByDescending(e => e.Count)
.Limit(count)
.ToListAsync();
var result = dbResult.Select(t => t.Tag.AsString);
return result;
}
The problem you're facing can be basically simplified to below line of code:
var agg = collection.Aggregate().Project(x => x.Tag);
Where Tag is a string property in your model.
It appears that Aggregate() and all the MongoDB driver operators are closer to Aggregation Framework than C# syntax allows them to be.
Based on your code the result variable is supposed to be of type String which gets translated by the driver into MongoDB.Bson.BsonString however Aggregation Framework always returns BSON documents (single one in this case) so MongoDB .NET driver cannot handle such deserialization in the runtime (BsonDocument -> BsonString).
First workaround is obvious - return anything that resembles BSON document and can be deserialized from BsonDocument type like:
collection.Aggregate().Project(x => new { x.Tag });
and then map results in memory (same query is run behind the scenes)
Another approach: translate your query into LINQ using .AsQueryable() which allows you to return results in more flexible manner:
collection.AsQueryable().Select(x => x.Tag);
In both cases the query that's generated for my projection looks the same:
{aggregate([{ "$project" : { "Tag" : "$Tag", "_id" : 0 } }])}
A bit late but this had a similar issue and this would have solved it for me:
You will want to make an intermediate class to represent group { Tag = v.Key, Count = v.Count() } and then change the Project to this.
.Project(Builders<YourIntermediateClass>.Projection.Expression(x => x.Tag))

How to add new where condition in IQueryable when selecting a view model?

I am fairly new to C# and .Net, so apologies if something doesn't make sense, I will try my best to explain my problem.
I have two methods which are basically going to use the similar query but with slight differences. So instead of repeating the query in both methods I created a third private method which will return the common part of the query and then the functions can add more clauses in query as they require.
Here is a generic function which returns the IQueryable object with common part of the query
private IQueryable<OfferViewModel> GetOffersQueryForSeller(int sellerId)
{
return Db.Offers
.Where(o => o.Sku.SellerId == sellerId && o.IsActive && !o.IsDiscontinued)
.Select(o => new OfferViewModel
{
Id = o.Id,
Name = o.Sku.Name,
ImageUrl = o.Sku.ImageUrl ?? o.Sku.Upcq.Upc.ImageUrl,
QuantityName = o.Sku.QuantityName
});
}
Following are the two method which are reusing the IQueryable object
public async Task<List<OfferViewModel>> GetSellerOffers(int sellerId)
{
var query = GetOffersQueryForSeller(sellerId);
return await query.ToListAsync();
}
public async Task<List<OfferViewModel>> GetDowngradableSellerOffers(int sellerId)
{
var query = GetOffersQueryForSeller(sellerId);
return await query
.Where(o => o.Sku.Id == monthlySkuId)
.ToListAsync();
}
Now GetSellerOffers works just fine but GetDowngradableSellerOffers throws a run time error with message The specified type member 'Sku' is not supported in LINQ to Entities.. I asked around and one of the guys told me that I cannot add additional where after adding a select which uses a ViewModel because then my records will be mapped to ViewModel and LINQ will attempt to look up props of ViewModel instead of database columns.
Now I have two questions,
In the docs I read Entity Framework will only run query when I try to fetch the results with methods like ToList and if I haven't done that why it wouldn't allow me to apply conditions on database fields/
How can I reuse the common query in my scenario?
How about the following code:
(The type Offer should be replaced by the type of the Elements that Db.Offers holds)
private IQueryable<OfferViewModel> GetOffersQueryForSeller(int sellerId, Func<Offer,bool> whereExtension)
{
return Db.Offers
.Where(o => ... && whereExtension.Invoke(o))
.Select(o => new OfferViewModel { ... });
}
private IQueryable<OfferViewModel> GetOffersQueryForSeller(int sellerId)
{
return GetOffersQueryForSeller(sellerId, (o) => true);
}
And then call it in GetDowngradableSellerOffers like this:
public async Task<List<OfferViewModel>> GetDowngradableSellerOffers(int sellerId)
{
var query = GetOffersQueryForSeller(sellerId, (o) => o.Sku.Id == monthlySkuId);
return await query.ToListAsync();
}

Entity Framework SQL projection to class model

Am I doing it wrong ir it is not possible to project a certain data model to strongly typed model instead of anonymous object?
I have this Linq expression:
var kudosLogsQuery = _kudosLogsDbSet
.Include(log => log.Type)
.Include(log => log.Employee)
.Where(log =>
log.OrganizationId == options.OrganizationId &&
log.BasketId == null)
.Where(StatusFilter(options.Status))
.Where(TypeFilter(options.TypeId))
.Where(UserFilter(options.SearchUserId))
.Select(log => new Comment
{
Id = log.Id,
Created = log.Created,
Employee = new Employee
{
FirstName = log.Employee.Name
}
Type = new Type
{
Name = log.Type.Name
}
})
.OrderByDescending(log => log.Created)
.ToList()
TypeFilter, StatusFilter, UserFilter is either x => true or just an another filter by Status, Type or User.
Unfortunately it produces an SQL statement that takes all fields from that table and projects it on the application side. The conclusion was made by testing these Linq expressions on SQL profiler.
Question:
Am I doing something wrong, or SQL projection works with anonymous objects only?
Thanks

Order query by int from an ordered list LINQ C#

So I am trying to order a query by an int var that is in an ordered list of the same int vars; e.g. the query must be sorted by the lists order of items. Each datacontext is from a different database which is the reason i'm making the first query into an ordered list of id's based on pet name order, only the pet id is available from the second query's data fields, Query looks like:
using (ListDataContext syndb = new ListDataContext())
{
using (QueryDataContext ledb = new QueryDataContext())
{
// Set the order of pets by name and make a list of the pet id's
var stp = syndb.StoredPets.OrderBy(x => x.Name).Select(x => x.PetID).ToList();
// Reorder the SoldPets query using the ordered list of pet id's
var slp = ledb.SoldPets.OrderBy(x => stp.IndexOf(x.petId)).Select(x => x);
// do something with the query
}
}
The second query is giving me a "Method 'Int32 IndexOf(Int32)' has no supported translation to SQL." error, is there a way to do what I need?
LINQ to SQL (EF) has to translate your LINQ queries into SQL that can be executed against a SQL server. What the error is trying to say, is that the .NET method of IndexOf doesn't have a SQL equivalent. You may be best to get your data from your SoldPets table without doing the IndexOf part and then doing any remaining ordering away from LINQ to SQL (EF).
Something like this should work:
List<StoredPet> storedPets;
List<SoldPet> soldPets;
using (ListDataContext listDataContext = new ListDataContext())
{
using (QueryDataContext queryDataContext= new QueryDataContext())
{
storedPets =
listDataContext.StoredPets
.OrderBy(sp => sp.Name)
.Select(sp => sp.PetId)
.ToList();
soldPets =
queryDataContext.SoldPets
.ToList();
}
}
List<SoldPets> orderedSoldPets =
soldPets.OrderBy(sp => storedPets.IndexOf(sp.PetId))
Note: Your capitalisation of PetId changes in your example, so you may wish to look at that.
LinqToSql can't transalte your linq statement into SQL because there is no equivalent of IndexOf() method. You will have to execute the linq statement first with ToList() method and then do sorting in memory.
using (ListDataContext syndb = new ListDataContext())
using (QueryDataContext ledb = new QueryDataContext())
{
var stp = syndb.StoredPets.OrderBy(x => x.Name).Select(x => x.PetID).ToList();
// Reorder the SoldPets query using the ordered list of pet id's
var slp = ledb.SoldPets.ToList().OrderBy(x => stp.IndexOf(x.petId));
}
You can use this, if the list size is acceptable:
using (ListDataContext syndb = new ListDataContext())
{
using (QueryDataContext ledb = new QueryDataContext())
{
var stp = syndb.StoredPets.OrderBy(x => x.Name).Select(x => x.PetID).ToList();
var slp = ledb.SoldPets.ToList().OrderBy(x => stp.IndexOf(x.petId));
// do something with the query
}
}

Internal .NET Framework Data Provider error 1025

IQueryable<Organization> query = context.Organizations;
Func<Reservation, bool> predicate = r => !r.IsDeleted;
query.Select(o => new {
Reservations = o.Reservations.Where(predicate)
}).ToList();
this query throws "Internal .NET Framework Data Provider error 1025" exception but the query below does not.
query.Select(o => new {
Reservations = o.Reservations.Where( r => !r.IsDeleted)
}).ToList();
I need to use the first one because I need to check a few if statements for constructing the right predicate. I know that I can not use if statements in this circumstance that is why I pass a delegate as parameter.
How can I make the first query work?
While the other answers are true, note that when trying to use it after a select statement one has to call AsQueryable() explicitly, otherwise the compiler will assume that we are trying to use IEnumerable methods, which expect a Func and not Expression<Func>.
This was probably the issue of the original poster, as otherwise the compiler will complain most of the time that it is looking for Expression<Func> and not Func.
Demo:
The following will fail:
MyContext.MySet.Where(m =>
m.SubCollection.Select(s => s.SubItem).Any(expr))
.Load()
While the following will work:
MyContext.MySet.Where(m =>
m.SubCollection.Select(s => s.SubItem).AsQueryable().Any(expr))
.Load()
After creating the bounty (rats!), I found this answer, which solved my problem. (My problem involved a .Any() call, which is a little more complicated than this question...)
In short, here's your answer:
IQueryable<Organization> query = context.Organizations;
Expression<Func<Reservation, bool>> expr = r => !r.IsDeleted;
query.Select(o => new { Reservations = o.Reservations.Where(expr) })
.ToList();
Read the referenced answer for an explanation of why you need the local variable expr, and you can't directly reference another method of return type Expression<Func<Reservation, bool>>.
Thanks for pinging me. I guess I was on the right track after all.
Anyway, to reiterate, LINQ to Entities (thanks to Jon Skeet for correcting me when I got mixed up in my own thought process in the comments) operates on Expression Trees; it allows for a projection to translate the lambda expression to SQL by the QueryProvider.
Regular Func<> works well for LINQ to Objects.
So in this case, when you're using the Entity Framework, any predicate passed to the EF's IQueryable has to be the Expression<Func<>>.
I just experienced this issue in a different scenario.
I have a static class full of Expression predicates which I can then combine or pass to an EF query. One of them was:
public static Expression<Func<ClientEvent, bool>> ClientHasAttendeeStatus(
IEnumerable<EventEnums.AttendeeStatus> statuses)
{
return ce => ce.Event.AttendeeStatuses
.Where(a => a.ClientId == ce.Client.Id)
.Select(a => a.Status.Value)
.Any(statuses.Contains);
}
This was throwing the 1025 error due to the Contains method group call. The entity framework expected an Expression and found a method group, which resulted in the error. Converting the code to use a lambda (which can be implicitly cast to an Expression) fixed the error
public static Expression<Func<ClientEvent, bool>> ClientHasAttendeeStatus(
IEnumerable<EventEnums.AttendeeStatus> statuses)
{
return ce => ce.Event.AttendeeStatuses
.Where(a => a.ClientId == ce.Client.Id)
.Select(a => a.Status.Value)
.Any(x => statuses.Contains(x));
}
Aside: I then simplified the expression to ce => ce.Event.AttendeeStatuses.Any(a => a.ClientId == ce.Client.Id && statuses.Contains(a.Status.Value));
Had a similar problem. Library of ViewModels that look like this:
public class TagViewModel
{
public int Id { get; set; }
public string Name { get; set; }
public static Expression<Func<SiteTag, TagViewModel>> Select = t => new TagViewModel
{
Id = t.Id,
Name = t.Name,
};
This works:
var tags = await db.Tags.Take(10).Select(TagViewModel.Select)
.ToArrayAsync();
But, this won't compile:
var post = await db.Posts.Take(10)
.Select(p => new {
Post = p,
Tags = p.Tags.Select(pt => pt.Tag).Select(TagViewModel.Select)
})
.ToArrayAsync();
Because the second .Select is a mess - the first one is actually called off of an ICollection, which is not IQueryable, so it consumes that first Expression as a plain Func, not Expression<Func.... That returns IEnumerable<..., as discussed on this page. So .AsQueryable() to the rescue:
var post = await db.Posts.Take(10)
.Select(p => new {
Post = p,
Tags = p.Tags.Select(pt => pt.Tag).AsQueryable()
.Select(TagViewModel.Select)
})
.ToArrayAsync();
But that creates a new, weirder problem: Either I get Internal Framework...Error 1025, or I get the post variable with a fully loaded .Post property, but the .Tags property has an EF proxy object that seems to be used for Lazy-Loading.
The solution is to control the return type of Tags, by ending use of the Anonymous class:
public class PostViewModel
{
public Post Post { get; set; }
public IEnumerable<TagViewModel> Tags { get; set; }
Now select into this and it all works:
var post = await db.Posts.Take(10)
.Select(p => new PostViewModel {
Post = p,
Tags = p.Tags.Select(pt => pt.Tag).AsQueryable()
.Select(TagViewModel.Select)
})
.ToArrayAsync();

Categories