Implementing Composite Pattern with Entity Framework Code First

2 במרץ 2011

The full sample code can be downloaded here


Entity Framework allows us to map object relationships into a database. One of the useful relationship is the Composite Pattern.


Suppose we have implemented the following class diagram:


Address Book Class Diagram


As you can see this is a classical implementation of the Composite design pattern, where Group holds a collection of polymorphic items through the property Items of type BookItem.


We would like to save this model into a database using Entity Framework Code First.
According to ECCodeFirst coding convention guidelines we should define the following classes:

public abstract class BookItem
{
    public int ID { getset; }
    public virtual Group Parent { getset; }

    public abstract void Accept(IBookVisitor visitor);
}

public class Group : BookItem
{
    public string Name { getset; }
    public virtual ICollection<BookItem> Items { getset; }

    public Group()
    {
        this.Items = new List<BookItem>();
    }

    public override void Accept(IBookVisitor visitor) {…}
}

 
public class Person : BookItem
{
    public string FirstName { getset; }
    public string LastName { getset; }
    public virtual ICollection<Address> Addresses { getset; }

    public Person()
    {
        this.Addresses = new List<Address>();
    }

    public override void Accept(IBookVisitor visitor) {…}
}

 
public class Address
{
    public int ID { getset; }
    public string Value { getset; }
    public string Description { getset; }
    public virtual Person Parent { getset; }
    public int PersonID { getset; }

    public void Accept(IBookVisitor visitor) {…}
}

 
public class BookContext : DbContext
{
    public DbSet<BookItem> Items { getset; }

    public BookContext()
        : base("AddressBook")
    { }

    protected override void OnModelCreating(ModelBuilder model)
    {
        model.Entity<BookItem>().ToTable("BookItems");
        model.Entity<Person>().ToTable("Persons");
        model.Entity<Group>().ToTable("Groups");
        model.Entity<Address>().HasRequired(a => a.Parent)

				      .WithMany(p => p.Addresses);

        base.OnModelCreating(model);
    }

    public Group Root
    {
        get
        {
            return (from item in this.Items where item.Parent == null 

			 let g = item as Group select g).Single();
        }
    }

    public class Initializer : DropCreateDatabaseIfModelChanges<BookContext>
    {
        protected override void Seed(BookContext context)
        {
            Group root = new Group() { Name = "~" };
            context.Items.Add(root);

            Person ori = new Person() { FirstName = "Ori", LastName = "Calvo" };
            root.Items.Add(ori);
            ori.Addresses.Add(new Address() { Value = "ori.calvo@gmail.com" });
            ori.Addresses.Add(new Address() { Value = "ori_calvo@hotmail.com" });

            Group friends = new Group() { Name = "Friends" };
            root.Items.Add(friends);

            Person yossi = new Person() { FirstName = "Yossi", LastName = "Halul" };
            friends.Items.Add(yossi);

            context.SaveChanges();
        }
    }
}

 

Hosting above code in a simple client application yields the following error: "Two entities with possibly different keys are mapped to the same row. Ensure these two mapping fragments map both ends of the AssociationSet to the corresponding columns."
The error is quite surprising since we are using the exact guidelines published at ADO.NET team blog 


After some investigation it looks like EFCodeFirst is not able to handle correctly the composite pattern without additional help. If you remove the collection of BookItems inside Group and remove the Parent reference inside BookItem everything works just fine.


The magic solution is quite simple: Add a raw GroupID field (of type int) to the BookItem class. This way EFCodeFirst understands that Group has a one-to-many relationship with BookItem (beside the inheritance relationship).
Here is the updated BookItem code:

public abstract class BookItem
{
    public int ID { getset; }
    public virtual Group Parent { getset; }
    public int? GroupID { getset; }

    public abstract void Accept(IBookVisitor visitor);
}


This time, the client application completes successfully.
Please note that GroupID must be defined as nullable, else, you will get an error regarding a potential cyclic references.


 

Add comment
facebook linkedin twitter email

Leave a Reply

Your email address will not be published.

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

2 comments

  1. sebas12 בדצמבר 2012 ב 18:07

    what if i need to use a same person in 2 or more groups? is that possible?

    Reply
  2. Luis Braschi20 בנובמבר 2014 ב 20:18

    Very concise and objective. The way I like it. :)

    Reply