VB.NET Linq Left Join using Query Syntax - vb.net

I have a list of applications. Each application may or may not have a support record associated with it.
The only way I can join is to take the App Title and see if its equal to the LookupValue of Support(Product). But to get that property, i have to cast to a FieldLookupValue. When there is no associated support record, that is where the null reference error gets thrown on .MoveNext() inside the Linq query. Below is what currently works for apps that have associated support, but throws the error when they don't.
Dim q =
From a In apps.AsEnumerable()
Group Join s In support.AsEnumerable()
On a("Title").ToString() Equals CType(s("Product"), FieldLookupValue).LookupValue
Into Group
From ajs In Group.DefaultIfEmpty()
Select New With {
.Name = a("Title"),
.SupportEnd = IIf(ajs Is Nothing, Nothing, ajs("End"))
}
Any way I could compare anonymous types in the On Statement? I can't seem to get the syntax right on that, or maybe its not possible. I feel that could fix the null reference error. My unsuccessful attempt returned Equals cannot compare type with the value of type
Dim q =
From a In apps.AsEnumerable()
Group Join s In support.AsEnumerable()
On
(New With {Key .AppName = a("Title").ToString()}) Equals
(New With {Key .AppName = IIf(s Is Nothing, "nada", CType(s("Product"), FieldLookupValue).LookupValue)})
Into Group
From ajs In Group.DefaultIfEmpty()
Select New With {
.Name = a("Title"),
.SupportEnd = IIf(ajs("End") Is Nothing, Nothing, ajs("End"))
}
Any ideas as to how to get this join to work using one of the two failed methods above would be great.

Once I created a dummy default object based on a random selection from the support list (i chose index 0), I used that to compare to as shown. Then just used the Let statement to have a boolean easily accessible when selecting.
Dim q =
From a In apps.AsEnumerable()
Group Join s In support.AsEnumerable()
On a("Title").ToString() Equals CType(s("Product"), FieldLookupValue).LookupValue
Into Group
From ajs In Group.DefaultIfEmpty(support(0))
Let NoSupport As Boolean = IIf(ajs.Equals(support(0)), True, False)
Select New With {
.Name = a("Title"),
.SupportEnd = IIf(NoSupport, "No Support", ajs("End"))
}

Related

VB.NET - List.Contains always returns false when it should be true

I am more of a C# developer, and I am stuck on trying to implement something on my visual basic code;
TL;DR is I have a database table which holds all the IDs of Employees that have gone through a process. I want to be able to get those into a list, and see if any of the current members going through this process exist on this list, and if they do remove those members so they don't get reprocessed. The database table returns an object called of type "ProcessedTermedMember" which is a custom object with three string propertys (ID, FirstName, LastName)
I basically have this code here, which is returning false... I know that it is looking to see the Reference is equal by default with contains, so it would only be true when it is the exact same object, vs an object where all properties are the same. How can I override this?
Here is my code:
Dim termedMembers = (From ec In db.ECs
Where ec.AccountTypeCode = "WCS"
Where ec.AccountStatus = 5
Select ec).ToList()
Dim processedTermMembers = (From p In db.ProcessedTermedMembers
Select p).ToList()
For Each member In termedMembers
Dim term As ProcessedTermedMember = New ProcessedTermedMember With {.EmployeeID = member.EmployeeID, .FirstName = member.EmployeeFirstName, .LastName = member.EmployeeLastName}
If processedTermMembers.Contains(term) Then
termedMembers.Remove(member)
End If
Next
This 'If Then' statement in the loop always returns "false" how can I get it to return true if all properties in the "term" variable are equal to the properties of one of the list items in "processedTermMembers"?
Any help would be greatly appreciated.
This won't work for a couple of reasons, but namely because you are attempting to modify a collection inside of a For/Each loop.
The simplest solution would be to move the declaration of termedMembers to after the declaration of processedTermMembers and to add an additional where clause to termedMembers:
Dim processedTermMembers = (From p In db.ProcessedTermedMembers
Select p).ToList()
Dim termedMembers = (From ec In db.ECs
Where ec.AccountTypeCode = "WCS" AndAlso ec.AccountStatus = 5 AndAlso Not processedTermMembers.Any(Function(term) term.EmployeeID = ec.EmployeeID AndAlso term.FirstName = ec.EmployeeFirstName AndAlso term.LastName = ec.EmployeeLastName)).ToList()
What the modified LINQ statement does is pull in the records from db.ECs where the AccountTypeCode is not "WCS", the AccountStatus is not 5, and is not in the processedTermMembers collection.
Just some modification into #David code :
Dim processedTermMembers = (From p In db.ProcessedTermedMembers
Select p).ToList()
Dim termedMembers = (From ec In db.ECs
Where (ec.AccountTypeCode = "WCS" _
AndAlso ec.AccountStatus = 5 _
AndAlso Not (processedTermMembers.Any(Function(term) ec.EmployeeID.Equals(term.EmployeeID) AndAlso ec.EmployeeFirstName.Equals(term.FirstName) AndAlso ec.EmployeeLastName.Equals(term.LastName))))).ToList()

LINQ left join object reference not set

I'm trying to left join two tables:
Dim ausartpreise = From ausart In wpneu.Dataset.Tables(1).AsEnumerable
Group Join wp In wpneu.Dataset.Tables(0).AsEnumerable On ausart.Item(2) Equals wp.Item(0) _
And ausart.Item(3) Equals wp.Item(2)
Into joined_res = Group From joinedrow In joined_res.DefaultIfEmpty _
Select New With {.artnr = ausart.Item(0), .lfdnrkal = ausart.Item(1), .artnrhz = ausart.Item(2), .lfdnr1 = ausart.Item(3), _
.menge = ausart.Item(4), .m = ausart.Item(5), .wpneu = IIf(joinedrow Is Nothing, 0, joinedrow.Item(72))}
As you see, I catch, if there are no matches. But despite of that, I get an 'object reference not set' error on joinedrow.
Why?
Use If(joinedrow Is Nothing, 0, joinedrow.Item(72)) instead of IIf
IIF is not short circuited and will evaluate both the true and false parts no matter what joinedrow Is Nothing evaluates to. Even though it will only use one of the results. IF(...) was introduced later in vb.net that will only evalute the needed expression.

VB.NET Linq to Entities Query with child objects

Basically I have the follwing:
Dim ctx As New AdminCoreEntities
Dim roles = (From r In ctx.Roles where r.Name.StartsWith("cust") Select r) 'list of System.Linq.IQueryable(Of AdminCoreModel.Role)
Dim items = From i In ctx.QuickLinks.Include("Roles")
Where (i.TenantID = "470556ba-3574-4b01-a619-b85e9721b966" AndAlso i.Roles.Contains(roles))
Select New With {
i.ID,
i.Name,
.Roles = (From r In i.Roles Select New With {.Id = r.ID, .Name = r.Name})
}
The error i get when i run this is:
Unable to cast object of type System.Data.Objects.ObjectQuery`1[AdminCoreModel.Role] to type AdminCoreModel.Role
Basically I have a many to many situation and I try to get all the Quicklinks objects queried by their roles
and not quite sure why EF will cast to a single AdminCoreModel.Role when i.Roles is a collections of objects.
Any help greatly appreciated
Well, .Contains() expects one Role, and you're passing it a list. That's what the error says.
Try (apologies in advance if I mangle the VB.NET syntax; I don't usually write VB.NET):
Dim ctx As New AdminCoreEntities
Dim items = From i In ctx.QuickLinks ' You don't need Include because you're projecting.
Where (i.TenantID = "470556ba-3574-4b01-a619-b85e9721b966"
AndAlso i.Roles.Any(Function(role) role.Name.StartsWith("cust"))
Select New With {
i.ID,
i.Name,
.Roles = (From r In i.Roles Select New With {.Id = r.ID, .Name = r.Name})
}

vb.net syntax for order by clause in linq to sql that has select new with

I have a LINQ to sql statement that joins 2 tables. I would like to add a order by clause on one of the columns. However the order by clause does not seem to take effect at all.
Could you please suggest the right syntax in VB.net to achieve order by in the following:
Dim query = From dtIt In dbsomecontext.mytable
Join dtIl In dbsomecontext.anothertable On dtIt.ItemID Equals dtIl.ItemID
Where dtIl.IsAvailable = True
Order By dtIt.manufacturer
Select New With {
.Alpha = UCase((dtIt.manufacturer).Substring(0, 1))
}
Dim dtManufacturer As DataTable = csLINQOperations.LINQToDataTable(query)
Return dtManufacturer
Have you put a break point on the line where to Dim dtManufacturer ?
I created some sample classes to repersent your data objects as you've defined it.
Dim linqQuery = From dtIT In myTables _
Join dtIL In otherTables On dtIT.ItemID Equals dtIL.ItemID _
Where dtIL.IsAvaliable = True _
Order By dtIT.Manufacturer Ascending _
Select New With {.Alpha = UCase((dtIT.Manufacturer).Substring(0, 1))}
Now, when I have a break point on the line after this LINQ Query I can inspect the object linqQuery by using "linqQuery.ToList" and see the order of the data. It does infact order the output in an ordered fashion, based on the Manufacturer name.
Why is it that you think your code is not ordering the data? Using the Break Points and Watch, inspect your "query" object (using "query.ToList" in the Quick Watch) and see if the results are ordered correctly.
Yes, I figured the results were not ordered by "QuickWatch"ing dtManufacturer in the line :
Dim dtManufacturer As DataTable = csLINQOperations.LINQToDataTable(query)
Now, I have changed the query to as follows and it works :
Dim query = From dtIt In dbinkAndToner.InkAndToners
Join dtIl In dbinkAndToner.ItemsLists On dtIt.ItemID Equals dtIl.ItemID
Where dtIl.IsAvailable = True
Select New With {
.Alpha = ((dtIt.Manufacturer).Substring(0, 1))
}
Distinct
query = From dtIt In query
Order By dtIt.Alpha
Dim dtManufacturer As DataTable = csLINQOperations.LINQToDataTable(query)
Return dtManufacturer

Dynamic query Linq to xml VB.NET

Hey,
I want to write a query that the "where" in the query is a string something like"
Dim query as string= "Name =xxxx and Date > 10 "
Dim t = from book in doc.Descendants("books") Select _
[Name] = book..value, [Date] = book..value....
Where (query)
I build the query string on run time
Thanks...
I'm not saying this is your case but I see this a lot from people that came from ASP classic where we used to build dynamic SQL strings all of the time. We tend to hope that LINQ will give us some more power in part of the code but let us use strings elsewhere. Unfortunately this isn't true. Where takes a boolean argument and there's no way around that. You can write your own parser that uses reflection and eventually returns a boolean but you'd be writing a lot of code that could be error prone. Here's how you really should do it:
Assuming this is our data class:
Public Class TestObject
Public Property Name As String
Public Property Job As String
End Class
And here's our test data:
Dim Objects As New List(Of TestObject)
Objects.Add(New TestObject() With {.Name = "A", .Job = "Baker"})
Objects.Add(New TestObject() With {.Name = "B", .Job = "President"})
Objects.Add(New TestObject() With {.Name = "C", .Job = "Bus Driver"})
Objects.Add(New TestObject() With {.Name = "D", .Job = "Trainer"})
What you want to do is create a variable that represents the data to search for:
''//This variable simulates our choice. Normally we would be parsing the querystring, form data, XML values, etc
Dim RandNum = New Random().Next(0, 3)
Dim LookForName As String = Nothing
Select Case RandNum
Case 0 : LookForName = "A"
Case 1 : LookForName = "B"
Case 2 : LookForName = "C"
End Select
''//Query based on our name
Dim Subset = (From O In Objects Select O Where (O.Name = LookForName)).ToList()
If sometimes you need to search on Job and sometimes and sometimes you don't you just might have to write a couple of queries:
Dim Subset As List(Of TestObject)
Select Case RandNum
Case 0
Subset = (From O In Objects Select O Where (O.Name = "A" And O.Job = "Baker")).ToList()
Case Else
Select Case RandNum
Case 1 : LookForName = "B"
Case 2 : LookForName = "C"
End Select
Subset = (From O In Objects Select O Where (O.Name = LookForName)).ToList()
End Select
And just to explain writing your own query parser (which is a path that I recommend you DO NOT go down), here is a very, very, very rough start. It only supports = and only strings and can break at multiple points.
Public Shared Function QueryParser(ByVal obj As Object, ByVal ParamArray queries() As String) As Boolean
''//Sanity check
If obj Is Nothing Then Throw New ArgumentNullException("obj")
If (queries Is Nothing) OrElse (queries.Count = 0) Then Throw New ArgumentNullException("queries")
''//Array of property/value
Dim NameValue() As String
''//Loop through each query
For Each Q In queries
''//Remove whitespace around equals sign
Q = System.Text.RegularExpressions.Regex.Replace(Q, "\s+=\s+", "=")
''//Break the query into two parts.
''//NOTE: this only supports the equal sign right now
NameValue = Q.Split("="c)
''//NOTE: if either part of the query also contains an equal sign then this exception will be thrown
If NameValue.Length <> 2 Then Throw New ArgumentException("Queries must be in the format X=Y")
''//Grab the property by name
Dim P = obj.GetType().GetProperty(NameValue(0))
''//Make sure it exists
If P Is Nothing Then Throw New ApplicationException(String.Format("Cannot find property {0}", NameValue(0)))
''//We only support strings right now
If Not P.PropertyType Is GetType(String) Then Throw New ApplicationException("Only string property types are support")
''//Get the value of the property for the supplied object
Dim V = P.GetValue(obj, Nothing)
''//Assumming null never equals null return false for a null value
If V Is Nothing Then Return False
''//Compare the two strings, return false if something doesn't match.
''//You could use String.Compare here, too, but this will use the current Option Compare rules
If V.ToString() <> NameValue(1) Then Return False
Next
''//The above didn't fail so return true
Return True
End Function
This code would allow you to write:
Dim Subset = (From O In Objects Select O Where (QueryParser(O, "Name = A", "Job = Baker"))).ToList()
No, there is nothing directly like what you're looking for where you can pass in a string. As they say, when all you have is a hammer, everything looks like a nail...The real problem is that you need to learn what LINQ is good at and apply it to your code (if it is a good fit), rather than try and make it do what you could with a dynamically built SQL query string.
What you should probably be doing is making those "Where" clauses strongly typed anyway. Your current code has a lot of potential to blow up and be hard to debug.
What you could do instead is something like this (sorry, using C#, been a while since I've touched VB.NET):
var query = from book in doc.Descendants("books")
select book;
if(needsNameComparison)
{
query = query.where(book.Name == nameToCompare);
}
if(needsDateComparison)
{
query = query.Where(book.Date > 10);
}
List<book> bookList = query.ToList();
With LINQ, "query" isn't actually run until the "ToList()" call. Since it uses late execution, the query is dynamic in that it's being built on until it actually needs to run. This is similar to the code you were looking to use before since you were building a query string ahead of time, then executing it at a specific point.