MongoDB and LINQ: "NOT IN" clause - c#

I have two collections, one is a list of image names, the second is a subset of that list. When a task has been completed its name is inserted into the second collection.
I need to retrieve a set of not yet completed image names from the first collection. I have achieved this successfully with:
var processedNames = processed.AsQueryable().Select(x => x.ImageName).ToArray();
foreach (var result in results.Where(x => !processedNames.Contains(x.ImageName))
However this brings a large list of strings back from the database and then sends it back to the database in a single document, which as well as being inefficient will break eventually.
So I tried to rewrite it so it's all performed server side with:
var results = from x in captures
join complete in processed.AsQueryable() on x.ImageName equals complete.ImageName into completed
where !completed.Any()
select x;
This fails with:
System.NotSupportedException: '$project or $group does not support {document}.'
I also tried using the non LINQ API:
var xs = capturesCollection.Aggregate()
.Lookup("Processed", "ImageName", "ImageName", #as: "CompletedCaptures")
.Match(x => x["CompletedCaptures"] == null)
.ToList();
This fails with:
MongoDB.Bson.BsonSerializationException: 'C# null values of type 'BsonValue' cannot be serialized using a serializer of type 'BsonValueSerializer'.'
How can I achieve this query completely server side with the C# driver? A pure LINQ solution is preferable for portability.

I worked out how to do it with the Aggregate API:
var results = capturesCollection.Aggregate()
.As<CaptureWithCompletions>()
.Lookup(processed, x => x.ImageName, x => x.ImageName, #as:(CaptureWithCompletions x) => x.CompletedCaptures)
.Match(x => !x.CompletedCaptures.Any())
//.Limit(2)
.ToList();

Related

Check if list exists in SharePoint Online without exception using CSOM

I am trying to delete a list in SharePoint Online by using the list title. But before that, I need to check if the list exists or not. I can use the method SharepointContext.Web.Lists.GetByTitle(title), but this throws an exception if the list does not exist. It does not return null. I don't want the execution to error out and throw an exception. Also, I don't want to use try-catch as that will result in a lot of try-catch blocks.
The second option I have is to do a query SharepointContext.LoadQuery(SharepointContext.Web.Lists.Where(list => list.Title == title))
and then execute the query and search collection for lists in there. But this results in an error that I cannot use the TItle expression in the query.
Does anyone know a good way to check if a list exists in SharePoint online and not throw exceptions if it does not?
public async Task<List> Get(Query query)
{
var library = SharepointContext.LoadQuery(
SharepointContext.Web.Lists
.Include(l => l.Title, l => l.Id)
.Where(l => l.Title == query.Title));
await SharepointContext.ExecuteQueryAsync();
return library;
}
NOTE:
I do not want to use the PnP library
I do not want to directly delete, I want to check before deletion
I do not want to load titles of all the lists and then do an in-memory comparison
The problem is with the way the comparison is being done in the query. For some reason, the Where query is unable to decipher the term query.Title. For some reason, it was unable to directly refer to the property inside of the query parameter passed to the method. But after we hold the value in a local variable and use that in the query, it works fine.
public async Task<List> Get(Query query)
{
var title = query.Title;
var library = SharepointContext.LoadQuery(
SharepointContext.Web.Lists
.Include(l => l.Title, l => l.Id)
.Where(l => l.Title == query.Title));
await SharepointContext.ExecuteQueryAsync();
return library;
}
The important bit here is var title = query.Title;

LINQ TO Entities OrderBy Conundrum

I am having trouble trying to understand how to perform an order by in a LINQ to Entities call to return data organized in the desired order. The database used is postgresql. The order by in postgres is:
SELECT
*
FROM
part
ORDER BY
split_part(partnumber, '-',1)::int
, split_part(partnumber, '-',2)::int
Partnumber is a string field which is formated into 2-3 segments which are numeric separated by '-'. eg:
1-000235
10-100364
9-123456
etc.
I would want the sorted result to return:
1-000235
9-123456
10-100364
I have a test VB.Net app I am trying to figure out how to do this:
Using ctx As New EFWeb.MaverickEntities
Dim myparts = ctx.parts.
OrderBy(Function(e) e.partnumber).
ToList()
For Each pt As part In myparts
Console.WriteLine("{0} - {1}", pt.partnumber, pt.description)
Next
End Using
I tried doing: CInt(e.partnumber.Split("-")(0)) to force sorting for the first segment of the partnumber, but errored out because of the the compiler did not like the array referrence for the result of the Split() call.
If anybody knows of a good current reference for LINQ to Entities ... that would be appreciated.
You didn't share your Linq code. Anyway I would get the data to client side and then do the ordering. In C#:
var result = ctx.Parts.AsEnumerable()
.Select(p => new {p, pnSplit = p.PartNumber.Split('-')})
.OrderBy(x => int.Parse(x.pnSplit[0]))
.ThenBy(x => int.Parse(x.pnSplit[1]))
.Select(x => x.p);
In VB it should be:
Dim result = ctx.Parts.AsEnumerable()
Select(Function(p) New With {p, .pnSplit = p.PartNumber.Split("-"c)}).
OrderBy(Function(x) Integer.Parse(x.pnSplit(0))).
ThenBy(Function(x) Integer.Parse(x.pnSplit(1))).
Select(Function(x) x.p)
Note the integer.Parse. Otherwise it would be alphabetic sort.

LINQ filter a database list with another database list

I have tried to find an answer for this.
I am using LINQ and trying to filter a database list with another list, to remove countries from a list of countries where a member is already a citizen.
var currentCitizenships = DbContext.Citizenships
.Where(c => c.MemberId == CurrentUser.MemberId)
.Include(c => c.Country)
.ToList();
var filtered = DbContext.Countries
.Where(c => !currentCitizenships.Any(current => current.Country.CountryId == c.CountryId));
I am getting a Not supported exception with the following message:
Unable to create a constant value of type 'Project.EntityFramework.Models.Citizenship'. Only primitive types or enumeration types are supported in this context.
Two Solutions Worked
Remove the ToList() of the first query.
The selected answer.
I've picked 1. due to using less lines and was a simpler solution with the same result.
It seems that it cannot create a valid SQL query using whatever is stored in currentCitizenships.
Get the list of country id's you need first, and then modify your query to use Contains on the simple collection of integers (or whatever CountryId is) instead.
var countryIds = currentCitizenships.Select(x => x.Country.CountryId).ToList();
var filtered = DbContext.Countries.Where(c => !countryIds.Contains(c.CountryId));

how to compare values from a list in Linq to Entity (any / contains)

There is a list, policiesToDelete of entity class, MonitoringRelations. Out of this list I have selected two elements and construed a new list:
var policyKeysToDelete = policiesToDelete
.Select(r => new {r.PolicyId, r.GroupId})
.ToList();
Now, I have a query where I want to compare elements from policyKeysToDelete list.
var objectsToDelete = (from p in storageContext.MonitoringRelations
where policyKeysToDelete
.Any(x => x == new {p.PolicyId, p.GroupId})
select p)
.ToList();
The problem: the query above throws this exception:
NotSupportedException: Unable to create a constant value of type 'Anonymous type'. Only primitive types or enumeration types are supported in this context.
I have tried changing the anonymous list to a list<tuple<PolicyId, GroupId>> , but that also didn't help, throwing the almost same exception. I tried using Contains in place of Any but that also didn't help.
Any idea how can I solve this problem?
EF cannot translate a list of complex objects into the SQL query. What EF can do, is translate a list of simple values into SQL when you use it with the .Contains method.
So if you extract a list of PolicyId's and a list of GroupId's from the policyKeysToDelete, and use it to select as much as you can with EF, then you can do the full check in the resultset which is then in-memory, using Linq-to-objects.
Warning: you are extracting too much from the database, so depending on the amount of data, a different solution might be better.
var policyKeysToDelete = policiesToDelete
.Select(r => new {r.PolicyId, r.GroupId})
.ToList();
// List of values types, which can be translated to SQL
var policyIds = policyKeysToDelete.Select(x => x.PolicyId).ToList();
var groupIds = policyKeysToDelete.Select(x => x.GroupId).ToList();
var objectsToDelete = storageContext.MonitoringRelations
// Do the part that we can do in the database, which is select the records
// which have an corresponding PolicyId or GroupId
.Where(x => policyIds.Contains(x.PolicyId) || groupIds.Contains(x.GroupId))
// Use this method to indicate that whatever follows after should not be
// translated to SQL
.AsEnumerable()
// Do the full check in-memory
.Where(x => policyKeysToDelete
.Any(y => x.PolicyId == y.PolicyId && x.GroupId == y.GroupId)
)
.ToList();

Last and LastOrDefault not supported

I am trying to get the first and last values in a list. The query operator First() is supported but Last() and LastOrDefault() give an error. Am I using the Last() operator incorrectly?
var purchaseBills = db.PurchaseBills.OrderBy(p => p.BillID);
if (purchaseBills.Count() >0)
{
var firstBill = purchaseBills.First(); // This is supported
// Attempt 1
var lastBill = purchaseBills.Last(); // Not supported
// Attempt 2
var lastBill = purchaseBills.LastOrDefault(); // Not supported
//Attempt 3
var lastBill = purchaseBills.Reverse().First(); // Not supported
textBoxPurchaseBillFrom.Text = firstBill.BillNo.ToString();
textBoxPurchaseBillTo.Text = lastBill.BillNo.ToString();
}
Update:
--Errors--
Attempt 1: The query operator 'Last' is not supported.
Attempt 2: The query operator 'LastOrDefault' is not supported.
Attempt 3: The query operator 'Reverse' is not supported.
Instead of putting it into an own list by calling ToList() or ToArray() i would prefer to use AsEnumerable().
Additionally like the others you should try OrderByDescending()
Instead of Count() i would use Any().
either you switch your OrderBy to
.OrderByDescending(p => p.BillID)
(and use first) or you do something like
purchaseBills.ToArray().Last()
if this is not to expensive.
Last is not supported by the back-end DB. You should try other techniques:
Run your query using OrderByDescending so your requested item comes first.
Code your LINQ query as usual, but enforce Linq2Sql to render it to a CLR collection and then you'll have free access to everything locally, including Last. Example:
var bills = purchaseBills.ToList();
var last = bills.Last();
The problem is that there's no easy translation into SQL for Last or Reverse, so either convert it to something in memory (ToList, ToArray) if there aren't going to be too many records, or you could run the query a 2nd time, with OrderByDescending instead of OrderBy and use First.
I try not to use LastOrDefault() because sometime it doesn't work or support.
I'm sorting id by desc and then grab the first records.
.OrderByDescending(o=>o.id)
.FirstOrDefault(s => s.Name == Name)
It has something to do with the fact that the Last operator is trying to be sent to the SQL server which has no corresponding command. Once solution is to put a ToArray() or Tolist() at the end of your db call which makes that one line an explicit call to get the data (instead of lazing loading on each of the other lines).
Yet another way get last element without orderbydescending and load all entities:
var lastBill = purchaseBills
.Where(f => f.BillID == purchaseBills.Max(f2 => f2.BillID))
.FirstOrDefault();
You can convert your IEnumerable into a List using the ToList() method, which will ensure that all your attempts are successful, as shown:
var purchaseBills = db.PurchaseBills.OrderBy(p => p.BillID);
if (purchaseBills.Any())
{
var firstBill = purchaseBills.First(); // This is supported
// Attempt 1
var lastBill = purchaseBills.ToList().Last();
// Attempt 2
var lastBill = purchaseBills.ToList().LastOrDefault();
//NoLogic
var lastBill = purchaseBills.ToList().Reverse().First();
textBoxPurchaseBillFrom.Text = firstBill.BillNo.ToString();
textBoxPurchaseBillTo.Text = lastBill.BillNo.ToString();
}

Categories