Is there a better way to do this LINQ statement block? - asp.net

I'm relatively new with LINQ, but I'm going to be getting into it a lot more. Is the following a practical application of LINQ, or is there a better way to do something like this?
Public Shared Function GetItems(ByVal itemsList As List(Of OrderItem),
ByVal whichForm As ItemsFor, ByVal formID As Integer) As List(Of OrderItem)
Dim items As New List(Of OrderItem)
Select Case whichForm
Case ItemsFor.MfrCredit
Dim query = From oi As OrderItem In itemsList _
Where oi.ManufacturerCreditID = formID Select oi
items = query
Case ItemsFor.CustomerCredit
Dim query = From oi As OrderItem In itemsList _
Where oi.CustomerCreditID = formID Select oi
items = query
Case ItemsFor.Invoice
Dim query = From oi As OrderItem In itemsList _
Where oi.InvoiceID = formID Select oi
items = query
Case ItemsFor.PurchaseOrder
Dim query = From oi As OrderItem In itemsList _
Where oi.PurchaseOrderID = formID Select oi
items = query
Case ItemsFor.Quote
Dim query = From oi As OrderItem In itemsList _
Where oi.QuoteID = formID Select oi
items = query
Case ItemsFor.StockingOrder
Dim query = From oi As OrderItem In itemsList _
Where oi.StockingOrderID = formID Select oi
items = query
End Select
Return items
End Function
I was thinking if I could get the property name somehow as an object I could just do one LINQ statement, but I'm not sure exactly how...
Thanks!

You could do something like:
Dim condition As Func(Of OrderItem, Boolean)
Select Case whichForm
Case ItemsFor.MfrCredit
condition = Function(oi As OrderItem) oi.ManufacturerCreditID = formID
Case ItemsFor.CustomerCredit
condition = Function(oi as OrderItem) oi.CustomerCreditID = formID
...
End Select
Return items.Where(condition).ToList()
It's not perfect but at least it's less code duplication...

You could use the Predicate delegate. I am imagining an array or list of Predicates, one for each ItemsFor.
Then your query is
Dim query = From oi As OrderItem In itemsList Where predicate select oi
See also this article on building predicates.
And this article on PredicateBuilder.

You can use LINQ expressions, like this: (My VB is rusty, so this might not compile)
Dim param = Expression.Parameter(GetType(OrderItem), "item")
Dim getter = Expression.Lambda(Of Func(Of OrderItem, Integer))( _
Expression.Property(param, whichForm.ToString()), _
param _
).Compile()
Return items.Where(Function(item) getter(item) == formId)
For optimal performance, cache the generated delegates in a Dictionary(Of ItemsFor, Func(Of OrderItem, Integer)).
EDIT:
The System.Linq.Expressions namespace allows you to create functions at runtime. This coe uses the feature to create a function that gets a property. Since the Compile method (which actually creates the function) is somewhat slow, it's better to reuse each generated delegate.

one way that you could turn that into a single query would be something like:
Dim query = From oi As OrderItem In itemsList _
Where ((whichForm = ItemsFor.MfrCredit) and (oi.ManufacturerCreditID = formID)) _
or ((whichForm = ItemsFor.CustomerCredit) and (oi.CustomerCreditID = formID)) _
or ((whichForm = ItemsFor.Invoice) and (oi.InvoiceID = formID)) _
...
select oi
items = query

There is a nice article here on Dynamic searchs in Linq with VB by creating an expression tree manually...quite a bit of work. Or another (better?) option is this article on Dynamic Linq (examples here are in C#)

Related

Using Results Outside of dbContext in VB.NET

Lets say I have code like this:
Using dbContext as mydb_entities = New mydb_entities
Dim qperson = (From p in dbContext.People _
Where p.name = "John" _
Select p)
End Using
Using dbContext as yourdb_entities = New yourdb_entities
Dim qyou = (From p in dbContext.Customer _
Where p.name = "John" _
Select p)
End Using
How can I compare the results of qperson to qyou? Since the results "disappear" once End Using is executed?
You will need to declare both variables outside the using statements
Dim qperson As IQueryable(Of Person)
Dim qyou As IQueryable(Of Customer)
Using dbContext as mydb_entities = New mydb_entities
qperson = (From p in dbContext.People _
Where p.name = "John" _
Select p)
End Using
Using dbContext as yourdb_entities = New yourdb_entities
qyou = (From p in dbContext.Customer _
Where p.name = "John" _
Select p)
End Using
The key thing here is to know when your LINQ queries are going to get run. When this line of code is executed:
qperson = (From p in dbContext.People _
Where p.name = "John" _
Select p)
no query is sent to the server. Instead you get back an object that implements the IQueryable(Of T) interface that describes what the query is. The query isn't actually sent to the server and executed until you start to use the results, such as in a For Each loop. This is called delayed execution and is fundamental to LINQ.
So what does this mean to you? Well, it means that the context must not be disposed before you execute the query. In the examples so far this is not necessarily always true. (The nested answer might do so, depending what is actually happening inside the nested usings.)
The typical way to deal with this is to force execution of the query to produce an in-memory collection of the results before the context is disposed. The ToList() extension method is a common way to do this. So, for example:
Dim qperson As IList(Of Person)
Dim qyou As IList(Of Customer)
Using dbContext as mydb_entities = New mydb_entities
qperson = (From p in dbContext.People _
Where p.name = "John" _
Select p).ToList()
End Using
Using dbContext as yourdb_entities = New yourdb_entities
qyou = (From p in dbContext.Customer _
Where p.name = "John" _
Select p).ToList()
End Using
Now you have executed the queries and got the results into memory before the contexts are disposed and you can happily do what you want with them.
In C# I just typically nest the usings...
using (var context blahentities())
{
using (var context2 blahentities())
{
}
}
Check this out for nesting usings in vb...
Nested using statements

LINQ to Entities does not recognize the method - simple delete statement

I have a GridView and on a row being deleted I trigger the GridView1_RowDeleting sub, but I receive an error "LINQ to Entities does not recognize the method 'System.Web.UI.WebControls.TableCell get_Item(Int32)' method, and this method cannot be translated into a store expression." Code is:
Private Sub GridView1_RowDeleting(sender As Object, e As System.Web.UI.WebControls.GridViewDeleteEventArgs) Handles GridView1.RowDeleting
' The deletion of the individual row is automatically handled by the GridView.
Dim dbDelete As New pbu_housingEntities
' Remove individual from the bed.
Dim remove_bed = From p In dbDelete.Beds _
Where p.occupant = GridView1.Rows(e.RowIndex).Cells(3).Text _
Where p.room = GridView1.Rows(e.RowIndex).Cells(6).Text _
Where p.building = GridView1.Rows(e.RowIndex).Cells(5).Text _
Order By p.id Descending _
Select p
remove_bed.First.occupant = ""
dbDelete.SaveChanges()
' Increase number of open spaces in room.
Dim update_occupancy = From p In dbDelete.Rooms _
Where p.room1 = GridView1.Rows(e.RowIndex).Cells(6).Text
Where p.building = GridView1.Rows(e.RowIndex).Cells(5).Text _
Select p
update_occupancy.First.current_occupancy = update_occupancy.First.current_occupancy - 1
dbDelete.SaveChanges()
End Sub
The specific line erroring out is:
remove_bed.First.occupant = ""
That's because the Linq query is translated to SQL, and there is no way to translate GridView1.Rows(e.RowIndex).Cells(3).Text to SQL. You need to extract the values from the GridView, then use them in the query
Dim occupant As String = GridView1.Rows(e.RowIndex).Cells(3).Text
Dim room As String = GridView1.Rows(e.RowIndex).Cells(6).Text
Dim building As String = GridView1.Rows(e.RowIndex).Cells(5).Text
Dim remove_bed = From p In dbDelete.Beds _
Where p.occupant = occupant _
Where p.room = room _
Where p.building = building _
Order By p.id Descending _
Select p
You have to put those values in variables before executing the query, otherwise the Entity Provider will try to pull the whole object into the query and access it's properties when it is trying to translate it into a SQL statement - which fails since there is no SQL equivalent.

Select query in LINQ based on Foreign Table

I have 2 Tables , OrderDetails and Requests In my LINQ to SQL dbml file.
OrderDetailsID is a foreign key in Requests Table.
I want to write the query to get the sum of UnitCost from OrderDetails based on OrderId.
And If there is a row in Requests Table for each OrderDetailsID, and the Requests.Last.RequestType="Refund" I want to reduce the total refund amount from the main sum otherwise If there is no row based on OrderDetailsID, add to sum.
Here is the way I implement that.
I am looking to prevent using "For each".
Any Solutions?
iRefund = (From od1 In dc.OrderDetails _
Where od1.OrderID =1 _
Select od1.UnitCost).Sum
Dim objOrderDetails = (From od1 In dc.OrderDetails _
Where od1.OrderID =1 _
Select od1)
For Each OrderDetail As ORM.Entities.OrderDetail In objOrderDetails
If Not OrderDetail.Requests Is Nothing Then
IF OrderDetail.Requests.Last.RequestType="Refund" Then
iRefund -= OrderDetail.UnitCost
End If
End If
Next
If you want to use Linq instead of a for-each loop, you could use the ForEach extension method of List:
Dim objOrderDetails = (From od1 In dc.OrderDetails _
Where od1.OrderId = 1 _
Select od1).ToList()
objOrderDetails.ForEach(AddressOf DecrementAmount)
Private Sub DecrementAmount(ByVal d As OrderDetail)
If d.Requests IsNot Nothing _
AndAlso d.Requests.Count > 0 _
AndAlso d.Requests.Last.RequestType = "Refund" Then
iRefund -= d.UnitCost
End If
End Sub
This assumes that iRefund is a module variable.

LINQ Query based on user preferences

How can I do this better (so it actually works : )
I have a LINQ Query that includes an order by that is based on a user preference. The user can decide if they would like the results ordered asc or desc.
If fuserclass.SortOrder = "Ascending" Then
Dim mydat = (From c In dc.ITRS Order By c.Date Ascending Select c)
Else
Dim mydat = (From c In dc.ITRS Order By c.Date Descending Select c)
End If
For each mydata in mydat ***<<<error "mydat is not declared"***
I know I could put my For Each loop inside the If and Else, but that seems silly to have the same code twice. I know you know of a better way : )
Define Dim mydat before the If statement. It is out of scope when your code reaches the for loop.
Use an extension method, and function-based LINQ (my VB is rusty as hell)
VB.NET:
<ExtensionAttribute> _
Public Shared Function OrderByString(Of TSource, TKey) ( _
source As IEnumerable(Of TSource), _
keySelector As Func(Of TSource, TKey) _
strType As String) As IOrderedEnumerable(Of TSource)
If strType = "Ascending" Then
return source.OrderBy(keySelector)
Else
return source.OrderByDescending(keySelector)
End If
End Function
C#.NET:
public static IOrderedEnumerable<TSource> OrderByString(
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector,
string strType)
{
if (strType == "Ascending")
return source.OrderBy(keySelector);
return source.OrderByDescending(keySelector);
}
Then call your query like this:
Dim mydat = dc.ITRS.OrderByString(Function(item) item.Date, fuserclass.SortOrder)
Or on C#:
var mydat = dv.ITRS.OrderbyString(item => item.Date, fuserclass.SortOrder);

How can I remove nodes from a SiteMapNodeCollection?

I've got a Repeater that lists all the web.sitemap child pages on an ASP.NET page. Its DataSource is a SiteMapNodeCollection. But, I don't want my registration form page to show up there.
Dim Children As SiteMapNodeCollection = SiteMap.CurrentNode.ChildNodes
'remove registration page from collection
For Each n As SiteMapNode In SiteMap.CurrentNode.ChildNodes
If n.Url = "/Registration.aspx" Then
Children.Remove(n)
End If
Next
RepeaterSubordinatePages.DataSource = Children
The SiteMapNodeCollection.Remove() method throws a
NotSupportedException: "Collection is read-only".
How can I remove the node from the collection before DataBinding the Repeater?
Your shouldn't need CType
Dim children = _
From n In SiteMap.CurrentNode.ChildNodes.Cast(Of SiteMapNode)() _
Where n.Url <> "/Registration.aspx" _
Select n
Using Linq and .Net 3.5:
//this will now be an enumeration, rather than a read only collection
Dim children = SiteMap.CurrentNode.ChildNodes.Where( _
Function (x) x.Url <> "/Registration.aspx" )
RepeaterSubordinatePages.DataSource = children
Without Linq, but using .Net 2:
Function IsShown( n as SiteMapNode ) as Boolean
Return n.Url <> "/Registration.aspx"
End Function
...
//get a generic list
Dim children as List(Of SiteMapNode) = _
New List(Of SiteMapNode) ( SiteMap.CurrentNode.ChildNodes )
//use the generic list's FindAll method
RepeaterSubordinatePages.DataSource = children.FindAll( IsShown )
Avoid removing items from collections as that's always slow. Unless you're going to be looping through multiple times you're better off filtering.
I got it to work with code below:
Dim children = From n In SiteMap.CurrentNode.ChildNodes _
Where CType(n, SiteMapNode).Url <> "/Registration.aspx" _
Select n
RepeaterSubordinatePages.DataSource = children
Is there a better way where I don't have to use the CType()?
Also, this sets children to a System.Collections.Generic.IEnumerable(Of Object). Is there a good way to get back something more strongly typed like a System.Collections.Generic.IEnumerable(Of System.Web.SiteMapNode) or even better a System.Web.SiteMapNodeCollection?

Resources