Understanding the Fluent Configuration API for One-To-Many Relationships in “Code-First” EF4

Configuring ORM’s through fluent api calls is relatively new to me. For the last three years or so I’ve been using EF1 and Linq-To-Sql as my data modeling tools of choice. My first exposure to a code-first ORM tool came in June when I started working with Fluent NHibernate. As interesting as that has been, I hadn’t really be faced with the issue of proper configuration because I’ve had someone on our team that does it easily. This weekend I started working on a sample project using the EF4 CTP, and the biggest stumbling block has been modeling the relationships.

The context code is a SCRUM process management app I’m writing. Here’s the model:

Model

The relationship I tried to model was the one between Project and Story via the Backlog property. Since “Project” is the first entity I needed to write, my natural inclination was to model the relationship to “Story” first.

  1. public class DataContext : DbContext
  2. {
  3.     public DbSet<Project> Projects { get; set; }
  4.     public DbSet<Resource> Resources { get; set; }
  5.     public DbSet<Sprint> Sprints { get; set; }
  6.     public DbSet<Story> Stories { get; set; }
  7.     public DbSet<StoryStatus> StoryStatuses { get; set; }
  8.     public DbSet<Task> Tasks { get; set; }
  9.  
  10.     protected override void OnModelCreating(System.Data.Entity.ModelConfiguration.ModelBuilder builder)
  11.     {
  12.         base.OnModelCreating(builder);
  13.  
  14.         builder
  15.             .Entity<Project>()
  16.             .HasMany(e => e.BackLog);

I knew that “Story” would be slightly more complex because it has two properties that map back to “Project.” These are the “Project” property ,and the “ProjectId” property. Some of the EF4 samples I found refer to a “Relationship” extension method that I was unable to find in the API, so I was fairly confused. I finally figured out what I needed to do by reading this post from Scott Hanselman, though he doesn’t specifically highlight the question I was trying to answer.

This is the mapping code I created for “Story:”

  1. builder.Entity<Story>()
  2.     .HasRequired(s => s.Project)
  3.     .HasConstraint((story, project) => story.ProjectId == project.Id);

and this is the code I’m using to create a new Story for a project:

  1. [HttpPost]
  2. public ActionResult Create(Story model)
  3. {
  4.     using (var context = new DataContext())
  5.     {
  6.         var project = context.Projects.Single(p => p.Id == model.ProjectId);
  7.         project.BackLog.Add(model);
  8.         context.SaveChanges();
  9.         return RedirectToAction("Backlog", "Story", new {id = project.Id});
  10.     }
  11. }

 

When I tried to save the story, the data context threw various exceptions. I thought I could avoid the problem by rewriting the code so that I was just adding the story directly to the “Stories” table on the DataContext (which I think will ultimately be the right thing as it saves an unnecessary database call), but that would have been hacking around the problem and not really understanding what was wrong with what I was doing. It just took some staring at Scott Hanselman’s code sample for awhile to realize what was wrong with my approach. Before I explain it, let me show you the mapping code that works.

  1. protected override void OnModelCreating(System.Data.Entity.ModelConfiguration.ModelBuilder builder)
  2. {
  3.     base.OnModelCreating(builder);
  4.  
  5.     builder
  6.         .Entity<Project>()
  7.         .Property(p => p.Title)
  8.         .IsRequired();
  9.  
  10.     // api no longer has Relationship() extension method.
  11.     builder.Entity<Story>()
  12.         .HasRequired(s => s.Project)
  13.         .WithMany(p => p.BackLog)
  14.         .HasConstraint((story, project) => story.ProjectId == project.Id);
  15.  
  16. }

Notice what’s missing? I completely yanked the mapping of “Project->Stories” from the “Project” model’s perspective. Instead, I map the one-to-many relationship from the child-entity’s perspective, i.e., “Story.” Here’s how to read the mapping.

  1. // api no longer has Relationship() extension method.
  2. builder.Entity<Story>() // The entity story
  3.     .HasRequired(s => s.Project) // has a required property called "Project" of type "Project"
  4.     .WithMany(p => p.BackLog) // and "Project" has a reference back to "Story" through it's "Backlog" collection property
  5.     .HasConstraint((story, project) => story.ProjectId == project.Id); // and Story.ProjectId is the same as Story.Project.Id
  6. ;

The key here is understanding that one-to-many relationships must be modeled from the perspective of the child-entity.

Leave a Reply

%d bloggers like this: