Here's a sample scenario that illustrates the problem I am having.
Here is the DB script to generate the database in SQL 2008:
USE [master]
GO
/****** Object: Database [EFTesting] Script Date: 08/15/2011 09:56:33 ******/
CREATE DATABASE [EFTesting] ON PRIMARY
( NAME = N'EFTesting', FILENAME = N'C:Program FilesMicrosoft SQL ServerMSSQL10.SQLEXPRESSMSSQLDATAEFTesting.mdf' , SIZE = 3072KB , MAXSIZE = UNLIMITED, FILEGROWTH = 1024KB )
LOG ON
( NAME = N'EFTesting_log', FILENAME = N'C:Program FilesMicrosoft SQL ServerMSSQL10.SQLEXPRESSMSSQLDATAEFTesting_log.ldf' , SIZE = 1024KB , MAXSIZE = 2048GB , FILEGROWTH = 10%)
GO
ALTER DATABASE [EFTesting] SET COMPATIBILITY_LEVEL = 100
GO
USE [EFTesting]
GO
/****** Object: Table [dbo].[Schedule] Script Date: 08/15/2011 09:45:53 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Schedule](
[ScheduleID] [int] IDENTITY(1,1) NOT NULL,
[Name] [nvarchar](50) NOT NULL,
[Version] [timestamp] NOT NULL,
CONSTRAINT [PK_Schedule] PRIMARY KEY CLUSTERED
(
[ScheduleID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
/****** Object: Table [dbo].[Customer] Script Date: 08/15/2011 09:45:53 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Customer](
[CustomerID] [int] IDENTITY(1,1) NOT NULL,
[Name] [nvarchar](50) NOT NULL,
[ScheduleID] [int] NOT NULL,
[Version] [timestamp] NOT NULL,
CONSTRAINT [PK_Customer] PRIMARY KEY CLUSTERED
(
[CustomerID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
/****** Object: ForeignKey [FK_Customer_Schedule] Script Date: 08/15/2011 09:45:53 ******/
ALTER TABLE [dbo].[Customer] WITH CHECK ADD CONSTRAINT [FK_Customer_Schedule] FOREIGN KEY([ScheduleID])
REFERENCES [dbo].[Schedule] ([ScheduleID])
GO
ALTER TABLE [dbo].[Customer] CHECK CONSTRAINT [FK_Customer_Schedule]
GO
And here is the C# code for the model, context, and test harness:
using System.ComponentModel.DataAnnotations;
using System.Data.Entity;
using System.Diagnostics;
using System.Linq;
namespace Tester
{
public class Context : DbContext
{
public Context(string connectionString) : base(connectionString)
{
Configuration.LazyLoadingEnabled = false;
Configuration.ProxyCreationEnabled = false;
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
// Customer
modelBuilder.Entity<Customer>()
.HasKey(c => c.ID)
.Property(c => c.ID).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity).HasColumnName("CustomerID");
modelBuilder.Entity<Customer>()
.Property(c => c.Version).IsConcurrencyToken();
modelBuilder.Entity<Customer>()
.HasRequired(c => c.Schedule);
modelBuilder.Entity<Customer>()
.ToTable("Customer");
// Schedule
modelBuilder.Entity<Schedule>()
.HasKey(s => s.ID)
.Property(s => s.ID).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity).HasColumnName("ScheduleID");
modelBuilder.Entity<Schedule>()
.Property(s => s.Version).IsConcurrencyToken();
modelBuilder.Entity<Schedule>()
.ToTable("Schedule");
}
}
public class Customer
{
public Customer()
{
Schedule = new Schedule();
}
public int ID { get; set; }
public string Name { get; set; }
public int ScheduleID { get; set; }
public Schedule Schedule { get; set; }
public byte[] Version { get; set; }
}
public class Schedule
{
public int ID { get; set; }
public string Name { get; set; }
public byte[] Version { get; set; }
}
public class Program
{
public static void Main(string[] args)
{
// create new customer / schedule
var context = new Context(@"Data Source=.SQLEXPRESS;Initial Catalog=EFTesting;Integrated Security=True;MultipleActiveResultSets=True");
var customer = new Customer
{
Name = "CUSTOMER",
Schedule = new Schedule
{
Name = "SCHEDULE"
}
};
context.Set<Customer>().Add(customer);
context.SaveChanges();
// pull new customer
context = new Context(@"Data Source=.SQLEXPRESS;Initial Catalog=EFTesting;Integrated Security=True;MultipleActiveResultSets=True");
var result = context.Set<Customer>().Include(c => c.Schedule).Single(c => c.ID == customer.ID);
// this succeeds
Debug.Assert(result.ScheduleID == customer.Schedule.ID);
// this fails - Schedule is not set to database version, is left as new version from constructor
Debug.Assert(result.Schedule.ID == customer.Schedule.ID);
}
}
}
You can see that a default Schedule instance is created inside the Customer constructor, so that it is never a null reference. However, the problem is that when EF loads the customer from the database, it doesn't bother to set the Schedule reference at all if it is not null.
This causes the foreign key property ScheduleID to be out of sync with the navigation property and will cause exceptions later on.
Can anyone explain why EF does this and if there is a way to work around it without changing the model design? It feels like a bug to me, even if this is by design, due to the fact that the model is not being kept synchronized by the framework.
See Question&Answers more detail:
os