Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
482 views
in Technique[技术] by (71.8m points)

entity framework 4.1 - EF 4.1 Code First: Why is EF not setting this navigation property?

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

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

I don't have a great answer to your question but I'll give it a shot anyway.

Basically, you can't initialize the Schedule property in the constructor. By doing this, EF thinks that the property has been modified (set to a new value) and will not attempt to overwrite it. In fact, if you add another context.SaveChanges() before the asserts in your code, you'll see that EF tries to insert the new Schedule entity you created in the constructor.

The only workaround I can suggest is to initialize the property manually outside the class or, maybe better, create an alternate Customer constructor and make the default one protected or private:

public class Customer
{
    public Customer(string name)
    {
        Name = name;
        Schedule = new Schedule();
    }

    protected Customer() { }

    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; }
}

EF will use the default constructor, but you can use the other one in your code.

I get what you mean about how this seems like a bug but I also kind of understand why EF does what it does... I guess I'm on the fence about it.

In any case, good luck!


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...