Should FirstOrDefault(lambda) and Where(lambda).FirstOrDefault() work the same? - entity-framework-4

I was under the impression FirstOrDefault(lambda) had the same effect on a collection as Where(lambda).FirstOrDefault() in LINQ to EF. Then I found this issue in code to find the most recently created item for a specified year from my DB
Private Function MostRecentItemByYear(year As Integer) As Item
Dim jan1 As New Date(year, 1, 1)
Dim dec31 As New Date(year, 12, 31)
Return MyContext.Items.OrderByDescending(Function(i) i.DateCreated) _
.FirstOrDefault(Function(i) i.DateCreated.HasValue AndAlso i.DateCreated >= jan1 AndAlso i.DateCreated <= dec31)
End Function
That code fails with
Unable to create a constant value of type 'MyCompany.Domain.Models.Item'. Only primitive types or enumeration types are supported in this context.
on the Return MyContext ... line. However, If I move the lambda to a Where and call FirstOrDefault separately, it runs perfectly.
Private Function MostRecentItemByYear(year As Integer) As Item
Dim jan1 As New Date(year, 1, 1)
Dim dec31 As New Date(year, 12, 31)
Return MyContext.Items.OrderByDescending(Function(i) i.DateCreated) _
.Where(Function(i) i.DateCreated.HasValue AndAlso i.DateCreated >= jan1 AndAlso i.DateCreated <= dec31) _
.FirstOrDefault()
End Function

Related

How can I populate a list with a default value for records that don't exist in a database?

I want to take records in a database over the past year, sum the records by month, and then populate a line graph with that information. However, when a month has no records, I can't seem to figure out how to get that into the proper location in the list. For example, if nothing exists for September or October, my line graph just skips those months. I tried checking and adding the months in afterwards, but I can't get them in sequential order. Any help?
Dim PointTotals = db.MemberRewards _
.Where(Function(r) sitewideFilterSelectedMemberIds.Contains(r.memberId) And r.supplier.name <> "AudStandard" And r.transactionDate >= startDate And r.transactionDate <= EndDate) _
.GroupBy(Function(r) New With {r.transactionDate.Value.Month, r.transactionDate.Value.Year}) _
.Select(Function(gr) New With {.month = gr.Key.Month, .year = gr.Key.Year, .totalPoints = gr.Sum(Function(r) r.points)}) _
.OrderBy(Function(gr) gr.year).ThenBy(Function(gr) gr.month)
Dim firstPeriodDate As Date
Dim currentDate As Date = DateAdd(DateInterval.Month, -1, startDate)
If PointTotals.Count > 0 Then
Dim firstPeriod = PointTotals.First
firstPeriodDate = CDate(firstPeriod.month & "/1/" & firstPeriod.year)
Else
firstPeriodDate = EndDate
End If
Dim months As New List(Of String)
Dim Points As New List(Of Integer)
Do While currentDate < firstPeriodDate
months.Add(currentDate.ToString("MMM"))
Points.Add(0)
currentDate = DateAdd(DateInterval.Month, 1, currentDate)
Loop
For Each period In PointTotals
months.Add(CDate(period.month & "/1/" & period.year).ToString("MMM"))
Points.Add(period.totalPoints)
Next
ViewBag.Months = """" & String.Join(""",""", months.ToArray) & """"
ViewBag.Points = String.Join(",", Points.ToArray)
I think this is a more elegant solution than trying to clean up the list after the loop. The only changes to your code are just before, and in, the For Each Period loop.
Dim PointTotals = db.MemberRewards _
.Where(Function(r) sitewideFilterSelectedMemberIds.Contains(r.memberId) And r.supplier.name <> "AudStandard" And r.transactionDate >= startDate And r.transactionDate <= EndDate) _
.GroupBy(Function(r) New With {r.transactionDate.Value.Month, r.transactionDate.Value.Year}) _
.Select(Function(gr) New With {.month = gr.Key.Month, .year = gr.Key.Year, .totalPoints = gr.Sum(Function(r) r.points)}) _
.OrderBy(Function(gr) gr.year).ThenBy(Function(gr) gr.month)
Dim firstPeriodDate As Date
Dim currentDate As Date = DateAdd(DateInterval.Month, -1, startDate)
If PointTotals.Count > 0 Then
Dim firstPeriod = PointTotals.First
firstPeriodDate = CDate(firstPeriod.month & "/1/" & firstPeriod.year)
Else
firstPeriodDate = EndDate
End If
Dim months As New List(Of String)
Dim Points As New List(Of Integer)
Do While currentDate < firstPeriodDate
months.Add(currentDate.ToString("MMM"))
Points.Add(0)
currentDate = DateAdd(DateInterval.Month, 1, currentDate)
Loop
Dim thisPeriodDate As Date
Dim previousPeriodDate As Date = currentDate
For Each period In PointTotals
thisPeriodDate = CDate(period.month & "/1/" & period.year)
Do While DateDiff(DateInterval.Month, previousPeriodDate, thisPeriodDate) > 1
months.Add(previousPeriodDate.ToString("MMM"))
Points.Add(0)
previousPeriodDate = DateAdd(DateInterval.Month, 1, previousPeriodDate)
Loop
months.Add(thisPeriodDate)
Points.Add(period.totalPoints)
previousPeriodDate = DateAdd(DateInterval.Month, 1, previousPeriodDate)
Next
ViewBag.Months = """" & String.Join(""",""", months.ToArray) & """"
ViewBag.Points = String.Join(",", Points.ToArray)
It sounds like that in your code to add missing months you used the .Add method. You would need to use months.Insert(position, CDate(....)) and Points.Insert(position, 0) where position is the correct index to insert the month in the correct order.
I could give you exact commands but you didn't include the cleanup code you referenced in the question.
There's a lot of approaches that you can take to solve this, I'll offer a DB one.
Since you're connecting to a database to pull your data, you can also do your summations/groupings on the DB side.
You can do this in 3 steps:
1) get a distinct list of all your months and store them in a temp table
2) create your summations by month, and store them in another temp table
3) left join step 1 on step 2 (using month as join criteria), order in whatever order you care about, and now you have all your months.
There are a lot of ways to implement this on the SQL side, the approach above was just one I thought would be easy to follow.

Inserting rows into existing Excel worksheet with OleDbConnection

I'm building insert statements based on a list of data and a tab name. I have 4 tabs, the last 2 get data inserted successfully, the first 2 do not.
I commented out inserting into all tabs but one. The size of the excel file increases, but the rows are still blank. Any ideas?
Edit: For some reason, the Excel file I was using as a "blank template" had "empty" values in rows of the first 2 sheets. First one had "empty values" in the first 100K rows, seconds had empty values in the first 700-some rows. The data was being inserted after these rows, which explains why the file size was increasing. Now I'm getting "Operation must use an updateable query" when trying to insert.
Public Sub BuildReport(Of T)(tabName As String, dataList As IEnumerable(Of T))
'// Setup the connectionstring for Excel 2007+ XML format
'// http://www.connectionstrings.com/ace-oledb-12-0/
If ((tabName.EndsWith("$") = True AndAlso m_TabList.Contains(tabName) = False) OrElse m_TabList.Contains(tabName & "$") = False) Then
Throw New Exception(String.Format("The specified tab does not exist in the Excel spreadsheet: {0}", tabName))
End If
Using excelConn As New OleDbConnection(m_ConnectionString)
excelConn.Open()
Dim insertStatementList As IEnumerable(Of String) = BuildInsertStatement(Of T)(tabName, dataList)
Using excelCommand As New OleDbCommand()
excelCommand.CommandType = CommandType.Text
excelCommand.Connection = excelConn
For Each insertStatement In insertStatementList
excelCommand.CommandText = insertStatement
excelCommand.ExecuteNonQuery()
Next
End Using
End Using
End Sub
Private Function BuildInsertStatement(Of T)(tabName As String, dataList As IEnumerable(Of T)) As IEnumerable(Of String)
Dim insertStatementList As New List(Of String)
Dim insertStatement As New StringBuilder()
For Each dataItem As T In dataList
Dim props As PropertyInfo() = GetType(T).GetProperties()
insertStatement.Clear()
insertStatement.AppendFormat("INSERT INTO [{0}$] ", tabName)
Dim nameValueDictionary As New Dictionary(Of String, String)
For Each prop As PropertyInfo In props
Dim excelColumn As ExcelColumnAttribute = CType(prop.GetCustomAttributes(GetType(ExcelColumnAttribute), False).FirstOrDefault(), ExcelColumnAttribute)
If (excelColumn IsNot Nothing) Then
Dim value As Object = prop.GetValue(dataItem, Nothing)
If (value IsNot Nothing AndAlso value.GetType() <> GetType(Integer) _
AndAlso value.GetType() <> GetType(Double) _
AndAlso value.GetType() <> GetType(Decimal) _
AndAlso value.GetType() <> GetType(Boolean)) Then
value = String.Format("""{0}""", value)
ElseIf (value Is Nothing) Then
value = "NULL"
End If
nameValueDictionary.Add(excelColumn.ColumnName, value)
End If
Next
Dim columList As String = String.Join(",", nameValueDictionary.Keys)
Dim valueList As String = String.Join(",", nameValueDictionary.Select(Function(x) x.Value))
insertStatement.AppendFormat("({0}) ", columList)
insertStatement.AppendFormat("VALUES ({0})", valueList)
insertStatementList.Add(insertStatement.ToString())
Next
Return insertStatementList
End Function
For some reason, the Excel file I was using as a "blank template" had "empty" values in rows of the first 2 sheets. First one had "empty values" in the first 100K rows, second one had empty values in the first 700-some rows. The data was being inserted after these rows, which explains why the file size was increasing. Now I'm getting "Operation must use an updateable query" when trying to insert.
I found the answer to the second problem here: Operation must use an updateable query when updating excel sheet
Just needed to remove the "IMEX=1" from the extended properties of the connection string which I added in trying to troubleshoot the issue.

Next not looping in vb.net

I have create a function:
Public Shared Function appid()
Dim appidQ = From v In db_apps.app_users
Where v.NT_id = System.Environment.UserName
Select v
For Each v In appidQ
Return Convert.ToInt32(v.app_id)
Next v
Return appid()
End Function
I am trying to return EVERY app_id for a user a place it in a viewdata (just for testing I will eventually be placing this info in a cookie.) Now this should be returning 2 records, as I currently have 2 records that meet the criteria of the query specified in appidQ. Upon debugging appidQ is indeed returning both records. However when I step into a little further the "For Each" doesn't loop so although appidQ is returning 2 records the function is only returning the first record. Any help would be greatly appreciated.
UPDATE
Obviously the Return appid() cause it to exit the loop here is my new code:
Public Shared Function appid()
Dim appidQ = From v In db_apps.app_users
Where v.NT_id = System.Environment.UserName
Select v
For Each v In appidQ
Dim var = v.app_id
Next v
Return appid()
End Function
But it still doesn't do what I need it to. I need it to return the results of
For Each v In appidQ
Dim var = v.app_id
Next v
As already noticed the return statement should be moved after the loop to get all the results.
Public Shared Function appid() As List(Of Int32)
Dim appidQ = From v In db_apps.app_users
Where v.NT_id = System.Environment.UserName
Select v
Dim result As New List(Of Int32)
For Each v In appidQ
result.Add(Convert.ToInt32(v.app_id))
Next v
Return result
End Function
EDIT
It seems from your comment that what you need is a String representation of the list. And when you try the ToString() extension, it returns the name of the object (ex: System.Collections.Generic.List1[System.Int32] So I don't know if there is a more efficient way, but this should work:
Public Shared Function appid() As String
Dim appidQ = From v In db_apps.app_users
Where v.NT_id = System.Environment.UserName
Select v
'Create a String representation
Dim stringList As New System.Text.StringBuilder
Dim counter As Integer = 0
For Each v In appidQ
stringList.Append(v.app_id.ToString())
'Separator
If Not counter = appidQ.Count - 1 Then
stringList.Append("; ")
End If
counter += 1
Next
Return stringList.ToString
End Function
You've got a Return statement in your loop.
Return is at the function level, so your loop (and function) will be exited after first passage in the loop.
You should do somtething like that (I'm not a VB guy).
Public Shared Function appid()
Return (From v In db_apps.app_users
Where v.NT_id = System.Environment.UserName
Select v).ToList()
.Select(Function(p) Convert.ToInt32(p.app_id)).ToList()
End Function
Then in your view, if you're using a ViewData, something like that.
For Each id As Int32 In CType(ViewData("foo"), List(Of Int32)
...
Next
Inside the loop, you Return, so you exit the function altogether (and the loop in particular).
If you do not Return, you will not exit the function, so you will have a chance to continue looping.
You REALLY should have Option Strict On. Since we have no idea what the data type of app_id is, and you have Option Strict Off, we can't tell what exactly we are supposed to be returning.
Try this:
Public Shared Function appid()
Return (From v In db_apps.app_users
Where v.NT_id = System.Environment.UserName
Select v.app_id).ToList()
End Function
It would be better if it were strongly-typed, but this should give you the result you want.
EDIT: If you need to convert to Int32, try this:
Public Shared Function appid()
Return (From v In db_apps.app_users
Where v.NT_id = System.Environment.UserName
Select v).ToList()
.Select(Function(p) Convert.ToInt32(p.app_id)).ToList()
End Function

Comparing Dates in Lambda Expression

I am trying to compare a date to a datetime field in SQL. I try this:
Dim entry = db.Tbl_Hydrations.Where(Function(x) x.Hyd_Create_Date.Date = _date1)
However, the error I get is:
The specified type member 'Date' is not supported in LINQ to Entities. Only initializers, entity members, and entity navigation properties are supported.
On line:
If entry.Any Then
Seems like LINQ to Entities doesn't support the Date property. You could try doing a comparison between two dates as a workaround:
Dim nextDay As DateTime = _date1.AddDays(1)
Dim entry = db.Tbl_Hydrations.Where(Function(x) x.Hyd_Create_Date >= _date1 AndAlso x.Hyd_Create_Date < nextDay)
not sure of the syntax (not a vb man, but you can use canonical functions)
Dim entry = db.Tbl_Hydrations
.Where(Function(x) EntityFunctions.CreateDateTime
(x.Hyd_Create_Date.Year, x.Hyd_Create_Date.Month, x.Hyd_Create_Date.Day, 0, 0, 0) = _date1);

Combining extension methods

I'm trying to write 2 extension methods to handle Enum types. One to use the description attribute to give some better explanation to the enum options and a second method to list the enum options and their description to use in a selectlist or some kind of collection.
You can read my code up to now here:
<Extension()> _
Public Function ToDescriptionString(ByVal en As System.Enum) As String
Dim type As Type = en.GetType
Dim entries() As String = en.ToString().Split(","c)
Dim description(entries.Length) As String
For i = 0 To entries.Length - 1
Dim fieldInfo = type.GetField(entries(i).Trim())
Dim attributes() = DirectCast(fieldInfo.GetCustomAttributes(GetType(DescriptionAttribute), False), DescriptionAttribute())
description(i) = If(attributes.Length > 0, attributes(0).Description, entries(i).Trim())
Next
Return String.Join(", ", description)
End Function
<Extension()> _
Public Function ToListFirstTry(ByVal en As System.Enum) As IEnumerable
Dim type As Type = en.GetType
Dim items = From item In System.Enum.GetValues(type) _
Select New With {.Value = item, .Text = item.ToDescriptionString}
Return items
End Function
<Extension()> _
Public Function ToListSecondTry(ByVal en As System.Enum) As IEnumerable
Dim list As New Dictionary(Of Integer, String)
Dim enumValues As Array = System.Enum.GetValues(en.GetType)
For Each value In enumValues
list.Add(value, value.ToDescriptionString)
Next
Return list
End Function
So my problem is both extension methods don't work that well together. The methods that converts the enum options to an ienumerable can't use the extension method to get the description.
I found all kind of examples to do one of both but never in combination with each other. What am I doing wrong? I still new to these new .NET 3.5 stuff.
The problem is that Enum.GetValues just returns a weakly typed Array.
Try this:
Public Function ToListFirstTry(ByVal en As System.Enum) As IEnumerable
Dim type As Type = en.GetType
Dim items = From item In System.Enum.GetValues(type).Cast(Of Enum)() _
Select New With {.Value = item, .Text = item.ToDescriptionString}
Return items
End Function
(It looks like explicitly typed range variables in VB queries don't mean the same thing as in C#.)

Resources