Why do my EntityFramework 6.1 Indexes using a custom DbInitializer not work? - entity-framework

I got a tiny model:
Public Class Thing
Public Property Id As Integer
Public Property Name As String
End Class
A matching DbContext:
Public Class Context
Inherits DbContext
Public Sub New()
MyBase.New("EfCodeFirstUniqueConstraintTest")
End Sub
Public Property Things As IDbSet(Of Thing)
Protected Overrides Sub OnModelCreating(modelBuilder As DbModelBuilder)
MyBase.OnModelCreating(modelBuilder)
BuildConstraints(modelBuilder)
End Sub
Private Sub BuildConstraints(modelBuilder As DbModelBuilder)
modelBuilder.Entity(Of Thing).Property(Function(m) m.Name) _
.HasMaxLength(255) _
.HasColumnAnnotation(IndexAnnotation.AnnotationName, _
New IndexAnnotation(New IndexAttribute("UniqueOrgUnitName") _
With {.IsUnique = True}))
End Sub
End Class
When I put this into a Solution with EF6.1 using this code:
Sub Main()
Using db As New Context
Dim t = New Thing With {.Name = "Thingy"}
db.Things.Add(t)
db.SaveChanges()
End Using
End Sub
everything works as expected. The 2. run will throw an exception for there is a unique index violation.
Unfortunately, I need my application to not throw away the whole DB. So I wrote a table and constraints dropping DbInitializer like this:
Public Class DbIniter
Implements IDatabaseInitializer(Of Context)
Public Sub InitializeDatabase(context As Context) Implements IDatabaseInitializer(Of Context).InitializeDatabase
DropAllTables(context)
Dim dbCreationScript = CType(context, IObjectContextAdapter).ObjectContext.CreateDatabaseScript()
context.Database.ExecuteSqlCommand(dbCreationScript)
CreateMetaDataTable(context)
Seed(context)
context.SaveChanges()
End Sub
Protected Sub DropAllTables(context As Context)
context.Database.ExecuteSqlCommand("EXEC sp_MSforeachtable 'ALTER TABLE ? NOCHECK CONSTRAINT all'")
Dim remainingTrys = 100
Dim everythingOk = False
While Not everythingOk AndAlso remainingTrys > 0
Try
context.Database.ExecuteSqlCommand("EXEC sp_MSforeachtable ""DECLARE #name nvarchar(max); SET #name = parsename('?', 1); EXEC sp_MSdropconstraints #name""")
context.Database.ExecuteSqlCommand("EXEC sp_MSforeachtable 'DROP TABLE ?'")
everythingOk = True
Catch ex As Exception
remainingTrys = remainingTrys - 1
End Try
End While
If Not everythingOk Then Throw New System.Data.Entity.Infrastructure.RetryLimitExceededException(String.Format("Database was not empty after last attempt."))
End Sub
Protected Sub CreateMetaDataTable(context As Context)
Dim sql = "CREATE TABLE dbo.__MigrationHistory ( MigrationId NVARCHAR(255) NOT NULL, CreatedOn DATETIME NOT NULL, Model VARBINARY(MAX) NOT NULL, ProductVersion NVARCHAR(32) NOT NULL);" _
& " ALTER TABLE dbo.__MigrationHistory ADD PRIMARY KEY (MigrationId); INSERT INTO dbo.__MigrationHistory (MigrationId, CreatedOn, Model, ProductVersion) VALUES ('InitialCreate', GetDate(), #p0, #p1);"
context.Database.ExecuteSqlCommand(sql, GetModel(context), GetProductVersion())
End Sub
Protected Function GetModel(context As Context) As Byte()
Using memoryStream As New MemoryStream
Using gzipStream As New GZipStream(memoryStream, CompressionMode.Compress)
Using writer = XmlWriter.Create(gzipStream, New XmlWriterSettings With {.Indent = True})
EdmxWriter.WriteEdmx(context, writer)
End Using
End Using
Return memoryStream.ToArray
End Using
End Function
Protected Overridable Sub Seed(context As Context)
End Sub
Protected Function GetProductVersion() As String
Return GetType(DbContext).Assembly.GetCustomAttributes(False).OfType(Of Reflection.AssemblyInformationalVersionAttribute).Single.InformationalVersion
End Function
End Class
Using this initializer, my indexes will never hit the database. Everything else works just fine.
ObjectContext.CreateDatabaseScript() will not return any SQL for the indexes:
create table [dbo].[Things] (
[Id] [int] not null identity,
[Name] [nvarchar](255) null,
primary key ([Id])
);
Using the default DbInitializer, the SQL sent to the DB looks like this:
CREATE TABLE [dbo].[Things] (
[Id] [int] NOT NULL IDENTITY,
[Name] [nvarchar](255),
CONSTRAINT [PK_dbo.Things] PRIMARY KEY ([Id])
)
followed by another statement like this:
CREATE UNIQUE INDEX [UniqueOrgUnitName] ON [dbo].[Things]([Name])
Does someone have any insight into why this does not work?
Where would the index-SQL come from when it is not included in what CreateDatabaseScript() does return?

I don't know exactly the history of EF but writing an EF provider (https://jetentityframeworkprovider.codeplex.com/) I've seen that some old EF versions used to generate object items using DbProviderServices interface. That interface (the interface you are using with your initializer) works on StoreItemCollection that I'm quite sure that does not contain information about indexes but only about EntitySet (entities) and AssociationSet (relationships). In the project above the implementation is in JetCreateSqlGenerator class.
Since some EF version, the EF does not use DbProviderServices anymore to generate database objects, or, better, if the EF Provider provides a MigrationSqlGenerator the EF uses that interface otherwise the EF use the old DbProviderServices. MigrationSqlGenerator provides Index generation and other migration utilities (for example column and table rename and so on).
So you don't see index generation in your custom migration because you are using the old interface.

Related

How to execute stored procedure from Entity Framework 6 (database first) and Npgsql?

Is this a bug?
My components are:
.NET 4.6.1
Entity Framework 6.0
Npgsql 3.0.5
PostgreSQL 9.5
First, I created a table and stored procedure in PostgreSQL 9.5
CREATE TABLE hello
(
msg text
)
WITH (
OIDS=FALSE
);
ALTER TABLE hello
OWNER TO postgres;
CREATE OR REPLACE FUNCTION sayhello()
RETURNS SETOF hello AS
$BODY$
select
*
from version()
$BODY$
LANGUAGE sql VOLATILE
COST 100
ROWS 1000;
ALTER FUNCTION sayhello()
OWNER TO postgres;
Second, I went to the .edmx file (Entity Framework 6.0), choose "update from database", selected the new table "hello" and the new stored procedure, "sayhello".
The Model Browser now shows the new table entity and the imported function.
Third, add a new procedure to the WCF file:
public string SayHello()
{
using (var ctx = new chaosEntities())
{
var x = ctx.sayhello();
return "Hello";
}
}
Set the WCF Service as Startup project and Start Debugging.
The WCF Test Client comes up. Executing SayHello() from the WCF Service leads to:
public virtual ObjectResult<hello> sayhello()
{
return ((IObjectContextAdapter)this).ObjectContext.ExecuteFunction<hello>("sayhello");
}
When this is executed, I get:
An exception of type 'System.Data.Entity.Core.EntityCommandCompilationException'
occurred in EntityFramework.dll but was not handled in user code
Additional information: An error occurred while preparing the command
definition. See the inner exception for details.
Inner Exception is: {"Value does not fall within the expected range."}
As I have several hundred stored procedures, any help on how to fix this is most appreciated.
TIA
Note: I suspect the problem is with NpgsqlServices.TranslateCommandTree, but I'm only guessing.
I could never get it to work the way I hoped (via EntityFramework), so I ended up doing this. I'd sure be open to a better solution!
The below code calls Npgsql directly to avoid the whole EntityFramework thing.
public string SayHello()
{
using (var ctx = new chaosEntities())
{
var b = ctx.Database.Connection.ConnectionString;
using (var conn = new Npgsql.NpgsqlConnection(connectionString: b))
{
conn.Open();
using (var tran = conn.BeginTransaction())
using (var command = conn.CreateCommand())
{
command.CommandText = "sayhello";
command.CommandType = CommandType.StoredProcedure;
var g = (string)command.ExecuteScalar();
return g;
}
}
}
}

Cannot drop a constraint in MS ACCESS

When using the SQL command :
ALTER TABLE [Sessions] DROP CONSTRAINT [SessionAttendance]
I get the exception error message "Could not find reference."
The constraint exists, and shows in the system table of constraints for this user table. How can I get this constraint to drop?
The database is in MS-ACCESS 2003 format. The application uses JET 4.0 I have several hundred instances which will need schema updates. I have a utility program to generate the SQL, but it falls over when attempting the DROP CONSTRAINT action.
Answered by implications of Gord Thompson in comment suggestions.
The ALTER statement was being applied to the wrong table in the relation.
The constraint was originally Added to the Attendance table. However it shows up as an attribute of the Sessions table when using the "GetOleDbSchemaTable" method to list.
Per the following code excerpt:
Structure Relation
Public Name As String
Public PrimaryTableName As String
Public PrimaryField As String
Public PrimaryIndex As String
Public ForeignTable As String
Public ForeignField As String
Public OnUpdate As String
Public OnDelete As String
Public Overrides Function ToString() As String
Dim msg As String = String.Format("Name:{0} PT:{1} PF:{2} PI:{3} FT:{4} FF:{5}", _
Name, PrimaryTableName, PrimaryField, PrimaryIndex, ForeignTable, ForeignField)
Return msg
End Function
End Structure
Private Function ListRelations(tableName As String) As List(Of Relation)
Dim relations As New List(Of Relation)
Dim MySchemaTable As DataTable
Dim dbConn As New OleDbConnection(connectionString)
dbConn.Open()
MySchemaTable = dbConn.GetOleDbSchemaTable(OleDbSchemaGuid.Foreign_Keys, _
New Object() {Nothing, Nothing, tableName})
Dim result As Boolean = False
'List the table name from each row in the schema table.
For Each row As DataRow In MySchemaTable.Rows
Dim r As New Relation
r.Name = row("FK_NAME")
r.PrimaryTableName = row("PK_TABLE_NAME")
r.PrimaryField = row("PK_COLUMN_NAME")
r.PrimaryIndex = row("PK_NAME")
r.ForeignTable = row("FK_TABLE_NAME")
r.ForeignField = row("FK_COLUMN_NAME")
r.OnUpdate = row("UPDATE_RULE")
r.OnDelete = row("DELETE_RULE")
Console.WriteLine(r.ToString)
relations.Add(r)
Next
MySchemaTable.Dispose()
dbConn.Close()
dbConn.Dispose()
Return relations
End Function

EF6 Database First making Stored Procedure Async

What's the right way to run a EF6 stored procedure (database-first) in async mode?
I read about ToListAsync() but I don't see that available on stored procedure.
Also not sure if there is a different way to call the stored procedure when the actual call returns (#1) an OUT param or a (#2) list of items:
Case #1
using (DBContext db = new DBContext())
{
ObjectParameter result = new ObjectParameter("Result",
typeof(global::System.Boolean));
db.Login("email#email.com", "password", result);
}
Case #2
using (DBContext db = new DBContext())
{
var result = db.Contact_GetList("New York");
}
Thanks for the help
As per this workitem you would need to use SqlQueryAsync. Feel free to upvote the work item on the EF Codeplex site.
To map stored procedures and start using it with out writing any initial code, this is how I did it.
create a new model with a new connection string that will generate the connection string automatically in the web.config file where the connection string is at (if you use a current connection string it may no work when you test the function for the SP on the model browser).
map your table and the stored procedures (you can test the stored procedures in the model browser).
create classes that represents the attributes retrieved by each stored procedure e.g if your stored procedure returns three columns A,B,C, then the class must also have these three columns as attribute with the [key()] on top of the column that is going to be the PK
now create your controller with the class created and a new DbContext
then copy the information in the data context generated for the model and pasted in the new context that you generate when creating the controller.
when you want to use the store procedures they will be ready on the db.context because you paste their code on you new db-context that you create when the controller was crated.
NOTE: I hope this is not confusing but I can use the stored procedures with out typing any code, please ask me if you need sample code or screen shots, your new db-context will not over write after you created
This is the stored procedure I map
'--------------------------------------------------------------------------
' <auto-generated>
' This code was generated from a template.
'
' Manual changes to this file may cause unexpected behavior in your application.
' Manual changes to this file will be overwritten if the code is regenerated.
' </auto-generated>
'-----------------------------------------------------------------------
Imports System
Imports System.Collections.Generic
Partial Public Class phone_CurrentConferences_Result
Public Property AppointmentID As Integer
Public Property AppTitle As String
Public Property DateTime As Nullable(Of Date)
Public Property [Date] As String
Public Property Time As String
Public Property Company As String
Public Property Contact As String
Public Property Phone As String
Public Property Office As String
Public Property Lead_Director As String
Public Property TBD As Nullable(Of Boolean)
Public Property conference As String
End Class
This is the same model with a primary key
Imports System
Imports System.Collections.Generic
Imports System.ComponentModel.DataAnnotations
Public Class Conferences
[Key]
Public Property AppointmentID As Integer
Public Property AppTitle As String
Public Property DateTime As Nullable(Of Date)
Public Property [Date] As String
Public Property Time As String
Public Property Company As String
Public Property Contact As String
Public Property Phone As String
Public Property Office As String
Public Property Lead_Director As String
Public Property TBD As Nullable(Of Boolean)
Public Property conference As String
End Class
This is the context generated by the EF
'--------------------------------------------------------------------------
' <auto-generated>
' This code was generated from a template.
'
' Manual changes to this file may cause unexpected behavior in your application.
' Manual changes to this file will be overwritten if the code is regenerated.
' </auto-generated>
'--------------------------------------------------------------------------
Imports System
Imports System.Data.Entity
Imports System.Data.Entity.Infrastructure
Imports System.Data.Entity.Core.Objects
Imports System.Linq
Partial Public Class DayMasterEntities
Inherits DbContext
Public Sub New()
MyBase.New("name=DayMasterEntities")
End Sub
Protected Overrides Sub OnModelCreating(modelBuilder As DbModelBuilder)
Throw New UnintentionalCodeFirstException()
End Sub
Public Overridable Function phone_CurrentConferences(number As String, [date] As Nullable(Of Date)) As ObjectResult(Of phone_CurrentConferences_Result)
Dim numberParameter As ObjectParameter = If(number IsNot Nothing, New ObjectParameter("number", number), New ObjectParameter("number", GetType(String)))
Dim dateParameter As ObjectParameter = If([date].HasValue, New ObjectParameter("date", [date]), New ObjectParameter("date", GetType(Date)))
Return DirectCast(Me, IObjectContextAdapter).ObjectContext.ExecuteFunction(Of phone_CurrentConferences_Result)("phone_CurrentConferences", numberParameter, dateParameter)
End Function
End Class
SO, when I create the controller I use the model with the <KEY()> and I create my own context that will look like this
Imports System.Data.Entity
Imports System.Data.Entity.Infrastructure
Imports System.Data.Entity.Core.Objects
Namespace Models
Public Class DayMasterContext
Inherits DbContext
' You can add custom code to this file. Changes will not be overwritten.
'
' If you want Entity Framework to drop and regenerate your database
' automatically whenever you change your model schema, please use data migrations.
' For more information refer to the documentation:
' http://msdn.microsoft.com/en-us/data/jj591621.aspx
Public Sub New()
MyBase.New("name=DayMasterEntities")
End Sub
Protected Overrides Sub OnModelCreating(modelBuilder As DbModelBuilder)
Throw New UnintentionalCodeFirstException()
End Sub
Public Property Conferences As System.Data.Entity.DbSet(Of Conferences)
End Class
End Namespace
Then I copy the info in the context generated by the EF to my context
Imports System.Data.Entity
Imports System.Data.Entity.Infrastructure
Imports System.Data.Entity.Core.Objects
Namespace Models
Public Class DayMasterContext
Inherits DbContext
' You can add custom code to this file. Changes will not be overwritten.
'
' If you want Entity Framework to drop and regenerate your database
' automatically whenever you change your model schema, please use data migrations.
' For more information refer to the documentation:
' http://msdn.microsoft.com/en-us/data/jj591621.aspx
Public Sub New()
MyBase.New("name=DayMasterEntities")
End Sub
Protected Overrides Sub OnModelCreating(modelBuilder As DbModelBuilder)
Throw New UnintentionalCodeFirstException()
End Sub
Public Overridable Function phone_CurrentConferences(number As String, [date] As Nullable(Of Date)) As ObjectResult(Of phone_CurrentConferences_Result)
Dim numberParameter As ObjectParameter = If(number IsNot Nothing, New ObjectParameter("number", number), New ObjectParameter("number", GetType(String)))
Dim dateParameter As ObjectParameter = If([date].HasValue, New ObjectParameter("date", [date]), New ObjectParameter("date", GetType(Date)))
Return DirectCast(Me, IObjectContextAdapter).ObjectContext.ExecuteFunction(Of phone_CurrentConferences_Result)("phone_CurrentConferences", numberParameter, dateParameter)
End Function
Public Property Conferences As System.Data.Entity.DbSet(Of Conferences)
End Class
End Namespace
So, now you can use this context to query
entConferences(number As String, [date] As Nullable(Of Date)) As ObjectResult(Of phone_CurrentConferences_Result)
or to get a DBSet(of conferences)
Here is a controller I have created with this technique
Look where I call my stored procedure
Dim conferences = db.phone_CurrentConferences(phoneNumber, currentDate)
Imports System.Data
Imports System.Data.Entity
Imports System.Data.Entity.Infrastructure
Imports System.Linq
Imports System.Net
Imports System.Net.Http
Imports System.Web.Http
Imports System.Web.Http.Description
Imports BIWEBAPI
Imports BIWEBAPI.Models
Namespace Controllers.DayMasterControllers
Public Class ConferencesController
Inherits System.Web.Http.ApiController
Private db As New DayMasterContext
' GET: api/Conferences
Function GetConferences() As IQueryable(Of Conferences)
Return db.Conferences
End Function
' GET: api/Conferences/3053742500
''' <summary>
''' Use to get the current conferences selected by date
''' </summary>
''' <param name="id">phone number and date separated by coma ",""</param>
''' <returns>conferences by date</returns>
''' <remarks></remarks>
<ResponseType(GetType(Conferences))>
Function GetConferences(ByVal id As String) As List(Of Conferences)
Dim conferencelist = New List(Of Conferences)
Dim dateAndPhoneNumber = Split(id, ",")
Dim currentDate = ""
Dim phoneNumber = dateAndPhoneNumber(0)
If dateAndPhoneNumber.Length > 1 Then
currentDate = DateTime.Parse(dateAndPhoneNumber(1))
Else : currentDate = DateTime.Today
End If
Dim conferences = db.phone_CurrentConferences(phoneNumber, currentDate)
For Each conferenceInQuery As Object In conferences
Dim conference = New Conferences()
conference.AppointmentID = conferenceInQuery.AppointmentID
conference.AppTitle = conferenceInQuery.AppTitle
conference.DateTime = conferenceInQuery.DateTime
conference.[Date] = conferenceInQuery.[Date]
conference.Time = conferenceInQuery.Time
conference.Company = conferenceInQuery.Company
conference.Contact = conferenceInQuery.Contact
conference.Phone = conferenceInQuery.Phone
conference.Office = conferenceInQuery.Office
conference.Lead_Director = conferenceInQuery.Lead_Director
conference.TBD = conferenceInQuery.TBD
conference.conference = conferenceInQuery.conference
conferencelist.Add(conference)
Next
Return conferencelist
End Function
' PUT: api/Conferences/5
<ResponseType(GetType(Void))>
Function PutConferences(ByVal id As Integer, ByVal conferences As Conferences) As IHttpActionResult
If Not ModelState.IsValid Then
Return BadRequest(ModelState)
End If
If Not id = conferences.AppointmentID Then
Return BadRequest()
End If
db.Entry(conferences).State = EntityState.Modified
Try
db.SaveChanges()
Catch ex As DbUpdateConcurrencyException
If Not (ConferencesExists(id)) Then
Return NotFound()
Else
Throw
End If
End Try
Return StatusCode(HttpStatusCode.NoContent)
End Function
' POST: api/Conferences
<ResponseType(GetType(Conferences))>
Function PostConferences(ByVal conferences As Conferences) As IHttpActionResult
If Not ModelState.IsValid Then
Return BadRequest(ModelState)
End If
db.Conferences.Add(conferences)
db.SaveChanges()
Return CreatedAtRoute("DefaultApi", New With {.id = conferences.AppointmentID}, conferences)
End Function
' DELETE: api/Conferences/5
<ResponseType(GetType(Conferences))>
Function DeleteConferences(ByVal id As Integer) As IHttpActionResult
Dim conferences As Conferences = db.Conferences.Find(id)
If IsNothing(conferences) Then
Return NotFound()
End If
db.Conferences.Remove(conferences)
db.SaveChanges()
Return Ok(conferences)
End Function
Protected Overrides Sub Dispose(ByVal disposing As Boolean)
If (disposing) Then
db.Dispose()
End If
MyBase.Dispose(disposing)
End Sub
Private Function ConferencesExists(ByVal id As Integer) As Boolean
Return db.Conferences.Count(Function(e) e.AppointmentID = id) > 0
End Function
End Class
End Namespace

EF code first: update entity

I've the following entity:
Partial Public Class Workflow
Sub New()
Activities = New List(Of WFActivity)
End Sub
<Key()>
Public Property ID As Long
Public Property Customer As Customer
<Required(), MaxLength(100)>
Public Property Name As String
Public Property Activities As List(Of WFActivity)
End Class
To add and update an entity I use the following procedure:
Public Sub SaveWorkflow(ByVal WF As Workflow)
Dim wfa As WFActivity
Try
Using ctx = New MyContext
ctx.Workflow.Add(WF)
If WF.ID > 0 Then
ctx.Entry(WF).State = EntityState.Modified
End If
For Each wfa In WF.Activities
If wfa.ID > 0 Then
ctx.Entry(wfa).State = EntityState.Modified
End If
Next
If WF.Customer.ID > 0 Then
ctx.Entry(WF.Customer).State = EntityState.Modified
End If
ctx.SaveChanges()
End Using
Catch ex As Exception
End Try
End Function
Inserting a new entity works fine. But using the same WF object for update purpose a second time with this procedure I got the following error:
An error occurred while saving entities that do not expose foreign key properties for their relationships. The EntityEntries property will return null because a single entity cannot be identified as the source of the exception. Handling of exceptions while saving can be made easier by exposing foreign key properties in your entity types. See the InnerException for details.
Where is the bug?
This happened to me once.
I think I solved it just by exposing the keys EF asks for
(Exposing the key means the entity holding the reference has also a property that contains the foreign key)
e.g:
public class Holder
{
public Held HeldObject{get;set;}
public int HeldID //this is the primary key for the entity Held (exact same name)
}
This will create a FK restriction in the DB

Does Entity Framework support circular references?

I have two entities in parent/child relationship. In addition, parent contains a reference to a "main" child, so the simplified model looks like this:
class Parent
{
int ParentId;
int? MainChildId;
}
class Child
{
int ChildId;
int ParentId;
}
The problem I am experiencing now is that EF does not seem to be able to handle creation of both Parent and Child in a single operation. I am getting an error "System.Data.UpdateException: Unable to determine a valid ordering for dependent operations. Dependencies may exist due to foreign key constraints, model requirements, or store-generated values."
MainChildId is nullable, so it should be possible to generate a parent, a child and then update a parent with the newly generated ChildId. Is this something that EF does not support?
No, it's supported. Try it with a GUID key or an assignable sequence. The error means exactly what it says it does: The EF can't figure out how to do this in one step. You can do it in two steps, though (two calls to SaveChanges()).
I had this exact issue. The apparent "Circular reference" is simply good database design. Having a flag on the child table like "IsMainChild" is bad design, the attribute "MainChild" is a property of the parent not the child, so an FK in the parent is appropriate.
EF4.1 needs to figure out a way to handle these type of relationships natively and not force us to redesign our databases to accommodate deficiencies in the framework.
Anyhow my workaround is to do it several steps (like you might when writing a stored procedure to do the same) the only wrinkle is to get round the change tracking on the context.
Using context As New <<My DB Context>>
' assuming the parent and child are already attached to the context but not added to the database yet
' get a reference to the MainChild but remove the FK to the parent
Dim child As Child = parent.MainChild
child.ParentID = Nothing
' key bit detach the child from the tracking context so we are free to update the parent
' we have to drop down to the ObjectContext API for that
CType(context, IObjectContextAdapter).ObjectContext.Detach(child)
' clear the reference on the parent to the child
parent.MainChildID = Nothing
' save the parent
context.Parents.Add(parent)
context.SaveChanges()
' assign the newly added parent id to the child
child.ParentID = parent.ParentID
' save the new child
context.Children.Add(child)
context.SaveChanges()
' wire up the Fk on the parent and save again
parent.MainChildID = child.ChildID
context.SaveChanges()
' we're done wasn't that easier with EF?
End Using
Both EF and LINQ to SQL have this problem of not being able to save circular references, even though they could be a lot more helpful by just encapsulating 2 or more SQL calls in a transaction behind the scenes for you instead of throwing an Exception.
I wrote a fix for this in LINQ to SQL but haven't gotten around to doing so in EF yet, because I've just been avoiding circular references in my db design for the time being.
What you can do is create a helper method that sets aside circular references, run that before calling SaveChanges(), run another method that puts the circular references back in place, and call SaveChanges() again. You can encapsulate all of that in a single method, maybe SaveChangesWithCircularReferences().
To put the circular references back, you need to track what you removed and return that log.
public class RemovedReference() . . .
public List<RemovedReference> SetAsideReferences()
{
. . .
}
So basically the code in SetAsideReferences is hunting down circular references, setting aside one half in each case, and recording those in a list.
In my case I created a class that stored the object, the property name, and the value (another object) that was removed, and just kept these in a list, like so:
public class RemovedReference
{
public object Object;
public string PropertyName;
public object Value;
}
There's probably a smarter structure to accomplish this; you could use a PropertyInfo object for example instead of a string, and you might cache the type to cheapen the second round of reflection.
This is an old question but still relevant with Entity Framework 6.2.0. My solution is three-fold:
Do NOT set the MainChildId column as HasDatabaseGeneratedOption(Computed) (this blocks you from updating it later)
Use a Trigger to update the Parent when I'm inserting both records simultaneously (this isn't a problem if the parent already exists and I'm just adding a new child, so be sure the Trigger accounts for this somehow - was easy in my case)
After calling ctx.SaveChanges(), also be sure to call ctx.Entry(myParentEntity).Reload() to get any updates to the MainChildId column from the Trigger (EF won't automatically pick these up).
In my code below, Thing is the parent and ThingInstance is the child and has these requirements:
Whenever a Thing (parent) is inserted, a ThingInstance (child) should also be inserted and set as the Thing's CurrentInstance (main child).
Other ThingInstances (children) may be added to a Thing (parent) with or without becoming the CurrentInstance (main child)
This resulted in the following design:
* EF Consumer must insert both records but leave CurrentInstanceId as null but be sure to set ThingInstance.Thing to the parent.
* Trigger will detect if a ThingInstance.Thing.CurrentInstanceId is null. If so, then it will update it to the ThingInstance.Id.
* EF Consumer must reload/refetch the data to view any updates by the trigger.
* Two round-trips are still necessary but only one atomic call to ctx.SaveChanges is necessary and I don't have to deal with manual rollbacks.
* I do have an extra trigger to manage, and there might be a more efficient way to do it than what I've done here with a cursor, but I'll never be doing this in a volume where performance will matter.
Database:
(Sorry, not tested this script - just generated it from my DB and put it here due to being in a hurry. You should definitely be able to get the important bits out of here.)
CREATE TABLE [dbo].[Thing](
[Id] [bigint] IDENTITY(1,1) NOT NULL,
[Something] [nvarchar](255) NOT NULL,
[CurrentInstanceId] [bigint] NULL,
CONSTRAINT [PK_Thing] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
CREATE TABLE [dbo].[ThingInstance](
[Id] [bigint] IDENTITY(1,1) NOT NULL,
[ThingId] [bigint] NOT NULL,
[SomethingElse] [nvarchar](255) NOT NULL,
CONSTRAINT [PK_ThingInstance] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[Thing] WITH CHECK ADD CONSTRAINT [FK_Thing_ThingInstance] FOREIGN KEY([CurrentInstanceId])
REFERENCES [dbo].[ThingInstance] ([Id])
GO
ALTER TABLE [dbo].[Thing] CHECK CONSTRAINT [FK_Thing_ThingInstance]
GO
ALTER TABLE [dbo].[ThingInstance] WITH CHECK ADD CONSTRAINT [FK_ThingInstance_Thing] FOREIGN KEY([ThingId])
REFERENCES [dbo].[Thing] ([Id])
ON DELETE CASCADE
GO
ALTER TABLE [dbo].[ThingInstance] CHECK CONSTRAINT [FK_ThingInstance_Thing]
GO
CREATE TRIGGER [dbo].[TR_ThingInstance_Insert]
ON [dbo].[ThingInstance]
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
DECLARE #thingId bigint;
DECLARE #instanceId bigint;
declare cur CURSOR LOCAL for
select Id, ThingId from INSERTED
open cur
fetch next from cur into #instanceId, #thingId
while ##FETCH_STATUS = 0 BEGIN
DECLARE #CurrentInstanceId bigint = NULL;
SELECT #CurrentInstanceId=CurrentInstanceId FROM Thing WHERE Id=#thingId
IF #CurrentInstanceId IS NULL
BEGIN
UPDATE Thing SET CurrentInstanceId=#instanceId WHERE Id=#thingId
END
fetch next from cur into #instanceId, #thingId
END
close cur
deallocate cur
END
GO
ALTER TABLE [dbo].[ThingInstance] ENABLE TRIGGER [TR_ThingInstance_Insert]
GO
C# Inserts:
public Thing Inserts(long currentId, string something)
{
using (var ctx = new MyContext())
{
Thing dbThing;
ThingInstance instance;
if (currentId > 0)
{
dbThing = ctx.Things
.Include(t => t.CurrentInstance)
.Single(t => t.Id == currentId);
instance = dbThing.CurrentInstance;
}
else
{
dbThing = new Thing();
instance = new ThingInstance
{
Thing = dbThing,
SomethingElse = "asdf"
};
ctx.ThingInstances.Add(instance);
}
dbThing.Something = something;
ctx.SaveChanges();
ctx.Entry(dbThing).Reload();
return dbThing;
}
}
C# New Child:
public Thing AddInstance(long thingId)
{
using (var ctx = new MyContext())
{
var dbThing = ctx.Things
.Include(t => t.CurrentInstance)
.Single(t => t.Id == thingId);
dbThing.CurrentInstance = new ThingInstance { SomethingElse = "qwerty", ThingId = dbThing.Id };
ctx.SaveChanges(); // Reload not necessary here
return dbThing;
}
}