Active Record with Entity Framework - vb.net

I'm working on a project that was built using ADO.NET (raw sql) and Active Record pattern. I am slowly moving it away from ADO.NET to Entity Framework Code First 4.3.
Here is an example of the pattern. The interface is a static Load and an instance Save (and Delete -- not shown).
Public Class Part
Public Property Id as Integer
Public Shared Function Load(_id As Integer) As Part
Using context As New DataContext()
Return context.Find(_id)
End Using
End Function
Public Sub Save()
Using context As New DataContext()
If Id = 0 Then
context.Parts.Add(Me)
Else
context.Entry(Me).State = Data.EntityState.Modified
End If
context.SaveChanges()
End Using
End Sub
End Class
I realize Active Record is not ideal for EF but I'd like to make it work to remove all of the ADO.NET code while not touching the rest of the code.
This mostly works, but I've run into an issue I don't know how to solve. In order to keep Foreign Keys in sync we handle it like such:
Public Sub Save()
ParentPart = Part.Load(ParentPartId)
ChildPart = Part.Load(ChildPartId)
Using context = New iTracContext()
If bid = 0 Then
context.BillOfMaterials.Add(Me)
Else
context.Entry(Me).State = Data.EntityState.Modified
End If
context.SaveChanges()
End Using
End Sub
This makes sure EF doesn't complain that we have non-matching relationships -- the Id always wins.
The issue is that its throwing an exception now when I save.
An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key.
This is thrown from the line:
context.Entry(Me).State = Data.EntityState.Modified
How is anything in the ObjectStateManager for this context? It is brand new and should be empty, no?
If I remove the two Part.Load(...) lines it works fine.
Is there some type of change tracker that lives outside the context that I'm not aware of? That seems like it would kill any attempt at the Active Record pattern.
I'm also open to any suggestions on how to make Active Record work with EF. The context.Entry line is terrible but I don't know what else to do.
Telling me not to do Active Record isn't helpful, but feel free.

I believe Entity Framework may still be tracking the object from the context you loaded it from, because you create a new context for each Load and Save call. If this is the case, try detaching the objects after you load them:
Public Shared Function Load(_id As Integer) As Part
Using context As New DataContext()
Part part = context.Find(_id)
context.Entry(part).State = EntityState.Detached ' Detach from the initial context
Return part
End Using
End Function

Related

list.add for a list (of ClassType) not adding to list - no errors reported

;tldr ...Orders.Add(order class variable) does not add a record the the Orders list, but also does not generate any errors.
I'll try to make a long story short and I apologize for the book you are about to read - I'm coming back to VB after spending a while in Python and C#. There is a bit of an explanation before any of the code...
I have a command-line exe program that at its base is working fine. We get 4 differently formatted CSV files that the program reads, and based on the file name, runs them through a parser that pulls the text information out of the file and populates a Public Class that in turn calls a function to populate a database table. The class is a single instance class named DataRecord. This program works absolutely fine (and has for well over 18 months now). It reads one line, if a batch# doesn't exist it creates one, and post the data class to the DB, and loops thru the file one line at a time doing so.
Also, their are 2 files to this program - the OrderAutomation main program file and the DBAccess file (which handle the DataRecord Class and all the various DB calls to verify information and to push the record into the database tables.
It used to be for all 4 file types, each file was a single batch all from a single region (data delimiters) - but now 1 of the file types will start sending multiple regions in a single file (and not necessarily in order) - so I need to modify the program that if its this 1 file type, it goes through the entire file, grouping each region together (each region will get its own Batch#).
I figured this would be fairly simple. I'd create an additional class (VTLData) that has some basic region separation data, and then a list of the DataRecord class. That looks like this:
Public Class VTLData
Public Property Locality As String
Public Property ARProvider As String
Public Property CORegion As String
Public Property BatchID As String
Public Property ProviderZip As String
Public Property IsValidRegion As Boolean
Public Property Orders As List(Of RecordData)
Public Sub New(s As String)
Locality = s
ARProvider = "00000000"
CORegion = "XX"
BatchID = "VXX-00000"
ProviderZip = "00000"
IsValidRegion = True
Orders = New List(Of RecordData)
End Sub
End Class
Like I said earlier - the RecordData class has no changes and is working fine.
There is a point in the program where I know this is the new file type (basically the region information has gone from a zip code to a set of specific text values) so when I test for that, if it's new, I set a boolean NewVTL to true. Because the file can contain more than one region, I have a VTLList property that is a List (of VTLData). I make sure I don't already have a Region in this list already - and if so, reference that index in the list.
When I get to the point where the program normally processes the record, I added the following (VTLIndex is the index variable for the list - at this time its value is 0):
If NewVTL Then
DB.data.CanProcess = CanProcessLine
DB.VTLList(VTLIndex).Orders.Add(DB.data)
Else
. - the old processing method
.
.
End If
I have a watch set for the VTLList(VTLIndex).Orders.Count value - that is at 0 when that line is reached, and stays at 0 when that line is processed.
I've tried creating a Push procedure in the VTLData class where you pass in the db.data class values, create a new instance of the record data and try to assign it that way
Public Sub PushOrder(item As RecordData)
Dim order As New RecordData
With order
.ARProviderNumber = ARProvider
.OrderID = item.OrderID
.AcctSuffix = item.AcctSuffix
.BatchID = BatchID
. - the 70 some other items in the recorddata class
.
.
End With
Orders.Add(order)
End Sub
This would also run without error, but not increase the count of .Orders.Count value.
I'm assuming its something stupid that I'm missing and I'm hoping someone can filter through all this and be like "You forgot to do blah..."
The only other thing I can think of is the do that Push function, but start it with Orders.Add(new RecordData) - and then modify the individual values through an index (.Orders(0).ARProviderNumber = data.ARProviderNumber... etc)
But based on other things I've done in VB - this should be working - I'm sure I'm just missing something...
So thanks for reaching this point - and thank you for letting me know what mistake I've made!!!
So the only way I got this to work for me was to create a Function in the VTLData class that did the following:
Public Function PushOrder(item As RecordData) As Boolean
Dim i As Integer = Orders.Count
Orders.Add(New RecordData)
With Orders(i)
.ARProviderNumber = ARProvider
.OrderID = item.OrderID
.AcctSuffix = item.AcctSuffix
.
. - more fields being populated
.
End With
If Orders.Count <= i Then Return False
Return True
End Function

Linq to Sql navigation object instance properties are null or default

I'm using Linq to Sql and have a proper foreign key relationship setup in the underlying tables.
However, when I try to use navigation properties I have a subtle bug.
In the code sample below, when I put a watch on the PartDetails, I do get the fully populated parts. However, if I call the property on each part to check their values, the instance is now null.
I've hunted around for the last couple of hours to find an answer but so far coming up dry.
Can anyone clue me in as to why this is happening?
I'm on .net 4.6.1, Visual studio 2015 and Sql Server 2014.
I confess I couldn't find the correct place to fire off the DataLoadOptions but this seemed to work fine!
Partial Public Class LabourDetail
Private Sub OnCreated()
Dim db As New DataContext
Dim ds As DataLoadOptions = New DataLoadOptions()
ds.LoadWith(Function(c As LabourDetail) c.PartDetails)
db.LoadOptions = ds
End Sub
Public ReadOnly Property AnyPartsUnConsumed As Boolean
Get
'If I put a watch on the partdetails I do get a proper collection with proper instances.
Return PartDetails.Where(Function(p) p.PartsUnConsumed).Any
End Get
End Property
End Class
Partial Public Class PartDetail
'When we reach this point, the values in the instance are all Null / Default
Public Property PartsUnConsumed() As Boolean = _CheckPartsUnConsumed()
End Class
I'd be grateful for any assistance!
This Private Sub OnCreated() effectively doesn't do anything. It creates a context that immediately goes out of scope.
I assume there is some context that materializes LabourDetails from the database. That's the context to set the LoadOptions of.

Late Binding to a form's public variables

I have 20+ MDI forms with consistently named Public variables. When the child form closes a method on the MDI Parent is called passing Me as a generic form type. How can I access the public variables by name via the Form reference? I only need to read the variables. Of course the Variables() method does not exist...
Public Sub CleanupForm(ByVal frm As Form)
Dim sTable_Name As String = frm.Variables("TABLE_NAME") ' Public at form level
Dim cLock As clsRecLocks
cLock = frm.Variables("Rec_Lock")
cLock.DeleteThisLock()
'..
I've seen some posts on similar requests but most start out with "don't do it that way..." then go off in the weeds not answering the question. I concede it is poor design. I can't change all the calling forms in the short term so I need to use this approach.
VS2010, VB.Net, Win Forms, .Net 2.0
I was able to get to a simple variable using CallByName:
Try
Dim s As String = CallByName(frm, "TABLE_NAME", CallType.Get)
Stop
Catch ex As Exception
MsgBox(ex.Message)
End Try
On to the class object. Perhaps I can add a default Get for the class that returns the ID I need.
Default property won't work as the Locks object was not declared Public - at least for the CallByName() approach.
Can Reflection get to form level variables not declared Public? Seems like a security issue, but...
Can I get a "Parent" reference in the instantiated Locks class? i.e. A reference to the form that established the Locks object? I can change the clsRecLocks() class.
I found a property I could get to that told me the form was "read-only" and I can use that tidbit to delete the correct (or more correct - still not 100%) lock record. So the bug is 90% fixed. I think I need update all the forms with code that records the info I need to get to 100%.
Thanks to all!
Poor design but you can do this:
Public Sub CleanupForm(ByVal frm As Form)
Dim frmTest as object = frm
? = frmTest.TABLE_NAME
? = frmTest.Rec_Lock
End Sub
This will compile and if the variables exist, it will return them but if not, you get an error.
Converting to an interface after the fact is not that hard, you should do it now rather than later.

Adding a new object to context

So apparently I don't understand EF well enough yet. Someone here at SO suggested I shouldn't keep a context object open for the life of my application, so I changed things slightly and now here is how it is currently working:
I read required data from the DB using EF and store it in an ObservableCollection(Of MyEntity).
The application keeps adding new objects to this collection, modifying/deleting existing objects in the ObservableCollection.
Upon application exit or Save button, I try to submit changes back to the DB, but apparently cannot. Here's the submission code.
For Each entity In MyObservableCollection
If entity.EntityState = EntityState.Added OrElse entity.EntityState = EntityState.Detached Then
Dim key = context.CreateEntityKey(entitySetName, entity)
entity.EntityKey = key
context.FilmTypes.Attach(entity)
ElseIf entity.EntityState = System.Data.EntityState.Modified Then
Dim originalItem As Object = Nothing
Dim key = context.CreateEntityKey(entitySetName, entity)
If context.TryGetObjectByKey(key, originalItem) Then
context.ApplyCurrentValues(key.EntitySetName, entity)
End If
End If
Next
context.SaveChanges(SaveOptions.AcceptAllChangesAfterSave)
EF tells me that there is already an object with the same key. My PK column is defined as Identity in the Model, so I expect it to auto-generate a temporary key (just like old good DataSets did).
How exactly do we work in "disconnected" mode in EF?
Found it myself. The code for Modified state is correct. For new objects, it should be like this:
If entity.EntityState = EntityState.Added OrElse entity.EntityState = EntityState.Detached Then
context.FilmTypes.AddObject(entity)
No need to specify key parameter. EF will take care of it.

Simultaneous threads only one seems to function at a time?

Threads a + b, (both are trying to delete files).
a gets called first, then b while a is still running.
b deletes the file successfully but a doesn't.
If I run a on its own, a's file deletes fine.
When I step through the code I can see that a's MultiAttemptFilename gets overwritten with b's.
I don't understand.
I have an ajax call pointing to a generic handler which passes the filename along with it.
In my handler I have the following code:
Dim Dc As New Document
Dim MyThread As New Thread(AddressOf Dc.DeleteFileMulitAttempt)
Dc.MulitAttemptFilename = Filename
MyThread.Start()
From my 'Document' class I'm calling the following:
#Region "Delete"
Public MulitAttemptFilename As String = ""
Public Sub DeleteFileMulitAttempt()
Dim TimeBetweenAttempts As Integer = 2000
Dim NumberOfAttempts As Integer = 60
Dim AttemptNumber As Integer = 0
Dim Success As Boolean = False
While (AttemptNumber < NumberOfAttempts)
Try
Success = (DeleteFile(MulitAttemptFilename) = "Ok")
Catch ex As Exception
Success = False
End Try
If (Success) Then Exit While
Thread.Sleep(TimeBetweenAttempts)
AttemptNumber += 1
End While
End If
End Sub
...
This is to handle cancelled/failed uploads as they don't always delete right away (server locks etc), hence the loop.
Am I missing something fundamental here?
It seems like you might be missing the fundamental concept of multi-threaded concurrency. There are books dedicated to this, and often sections of .NET books will address this issue. Here's just one article by Microsoft on the topic.
One short answer is you need to use VB's "lock" keyword. You create an object and you do roughly something like
lock(yourLockObject)
{
//any code that might access a shared resource like the variable
//MulitAttempFilename [sic] would go here.
}
I don't speak VB but it looks like you're making the one thing that really needs to be protected a global variable. Global data is pretty much a bad idea in any form and when it comes to multi-threading it's a really, really bad idea. You'll have to rewrite your code to protect access to the name of the file being deleted. While you're reading up on multi-threading you might also want to learn about thread pools.