locked
Mapping exclusive-or relationships reloaded using CTP5 RRS feed

  • Question

  • I need to map inherited navigation properties defined on abstract base classes, i.e. Orders in the following sample:

    public
     abstract
     class
     Customer
    {
    public virtual Guid Id { get ; set ; }
    public virtual string Name { get ; set ; }

    public virtual ICollection<Order> Orders { get ; set ; }
    }

    public class Account: Customer
    {
    public virtual string AccountNumber { get ; set ; }
    }

    public class Contact: Customer
    {
    public virtual string FirstName { get ; set ; }
    public virtual string LastName { get ; set ; }
    }

    public class Order
    {
    public Guid Id { get ; set ; }
    public string Name { get ; set ; }

    public Customer BillToCustomer { get ; set ; }
    public Guid BillToAccountId { get ; set ; }
    public Guid BillToContactId { get ; set ; }
    }

    It is known from answers in other forum threads that these base class navigation properties would result in associations to both derived types (Account and Contact in our case). In CTP4 I was not able to model conditional association using the Table Per Concrete class pattern.

    Here are the updated TPC mappings for the Account - Contact XOR relationships using CTP5:

     

    public
     class
     ContactMap : EntityTypeConfiguration<Contact>
    {
    public ContactMap()
    {
    HasMany(c => c.Orders)
    .WithOptional(o => (Contact)o.BillToCustomer)
    .HasForeignKey(o => o.BillToContactId) <br/>
    .WillCascadeOnDelete(true );

    Map(m =>
    {
    m.MapInheritedProperties();
    m.ToTable("Contacts" );
    });
    }
    }

    public class AccountMap : EntityTypeConfiguration<Account>
    {
    public AccountMap()
    {
    HasMany(a => a.Orders)
    .WithOptional(o => (Account)o.BillToCustomer)
    .HasForeignKey(o => o.BillToAccountId) <br/>
    .WillCascadeOnDelete(true );

    Map(m =>
    {
    m.MapInheritedProperties();
    m.ToTable("Accounts" );
    });
    }
    }

     

    With some trivial boilerplate on the property setters of Order (including some entity validation which is now possible in CTP5) we can "simulate" conditional associations:

     

    public
     class
     Order
    {
    private Customer _billToCustomer;
    private Guid? _billToAccountId;
    private Guid? _billToContactId;

    public Guid Id { get ; set ; }
    public string Name { get ; set ; }

    [Required]
    // NOTE: BillToAccountId and BillToContactId are disjoint & complete - validation rule to be inserted
    public Customer BillToCustomer {
    get
    {
    return _billToCustomer;
    }
    set
    {
    if (value is Account)
    {
    _billToContactId = null ; _billToAccountId = value.Id;
    _billToCustomer = value;
    }
    else // if (value is Contact)
    {
    _billToAccountId = null ;<br/>
     _billToContactId = value.Id;
    _billToCustomer = value;
    }
    }
    }

    public Guid? BillToAccountId {
    get
    {
    return _billToAccountId;
    }
    set
    {
    _billToContactId = null ;
    _billToAccountId = value;
    if (value.HasValue)
    {
    _billToCustomer = new Account { Id = value.Value }; }
    }
    }

    public Guid? BillToContactId
    {
    get
    {
    return _billToContactId;
    }
    set
    {
    _billToAccountId = null ;
    _billToContactId = value;
    if (value.HasValue)
    {
    _billToCustomer = new Contact { Id = value.Value }; }
    }
    }
    }

    The price you pay for this is that the abstract base class (Customer) has to be explicitly ignored (unmapped).

    1. Is there any way I can include Customers in the db context?
    2. Is there any way to avoid exposing FKs in the Order model above? Property hiding or mapping fields is not supported in EF4 AFAIK.
    3. Is there any way to avoid the boilerplate Order model code for property setters?
    4. In general, what are the changes - if any - wrt to abstract base classes with navigation properties in CTP5?

     Regards,


    Dimitris Foukas
    Wednesday, December 22, 2010 8:23 AM

Answers

  • Dimitris,

     

    Mapping a single navigation property to two different foreign keys is not something that is supported by core EF and is therefore also not supported by Code First since Code First builds on top of core EF.  It is possible to do something similar in core EF with TPC mapping where foreign key property/column is shared.  In this case there is one FK property/column but it refers to the PK in either Account table or the Contact table.  However, this currently doesn’t work with Code First in CTP5 because of some bugs in the TPC mapping code.  When the bugs are fixed something like this should work:

     

        public abstract class Customer

        {

            public virtual Guid Id { get; set; }

            public virtual string Name { get; set; }

     

            public virtual ICollection<Order> Orders { get; set; }

        }

     

        public class Account : Customer

        {

            public virtual string AccountNumber { get; set; }

        }

     

        public class Contact : Customer

        {

            public virtual string FirstName { get; set; }

            public virtual string LastName { get; set; }

        }

     

        public class Order

        {

            public Guid Id { get; set; }

            public string Name { get; set; }

     

            public Customer BillToCustomer { get; set; }

            public Guid BillToCustomerId { get; set; }

        }

     

        public class TpcRelationshipContext : DbContext

        {

            protected override void OnModelCreating(ModelBuilder modelBuilder)

            {

                modelBuilder.Entity<Order>();

                modelBuilder.Entity<Account>()

                    .Map(mc => { mc.MapInheritedProperties(); })

                    .ToTable("Accounts");

     

                modelBuilder.Entity<Contact>()

                    .Map(mc => { mc.MapInheritedProperties(); })

                    .ToTable("Contacts");

            }

        }

     

    Things to note about this are that first it doesn’t work yet due to some bugs and second even though EF is treating the BillToCustomerId property as an FK there isn’t a constraint for this in the database.  This is because the PK for this FK may be in one of two different tables depending on whether the relationship is to an Account or a Contact.

     

    I think that the main problem with the workaround you show is that the EF knows nothing about the FK properties or the navigation properties.  This means that all the internal logic that reasons about relationships for loading, fixup, creating update commands, and so on is not going to take these FKs or relationships into account.  This doesn’t mean that it won’t work, but there may be unexpected behavior because of it.

     

    Typically you can avoid exposing FKs in your model if you are willing to work with “independent associations”.  However, independent associations come with a lot of baggage and can make some things, such as working with N-Tier scenarios or doing concurrency resolution, very hard.  It’s usually best to use FK associations.  The IsIndependent methods in the relationship fluent API are used to setup a relationship as independent.

     

    Thanks,

    Arthur

    Wednesday, December 22, 2010 10:05 PM
    Moderator

All replies

  • Dimitris,

     

    Mapping a single navigation property to two different foreign keys is not something that is supported by core EF and is therefore also not supported by Code First since Code First builds on top of core EF.  It is possible to do something similar in core EF with TPC mapping where foreign key property/column is shared.  In this case there is one FK property/column but it refers to the PK in either Account table or the Contact table.  However, this currently doesn’t work with Code First in CTP5 because of some bugs in the TPC mapping code.  When the bugs are fixed something like this should work:

     

        public abstract class Customer

        {

            public virtual Guid Id { get; set; }

            public virtual string Name { get; set; }

     

            public virtual ICollection<Order> Orders { get; set; }

        }

     

        public class Account : Customer

        {

            public virtual string AccountNumber { get; set; }

        }

     

        public class Contact : Customer

        {

            public virtual string FirstName { get; set; }

            public virtual string LastName { get; set; }

        }

     

        public class Order

        {

            public Guid Id { get; set; }

            public string Name { get; set; }

     

            public Customer BillToCustomer { get; set; }

            public Guid BillToCustomerId { get; set; }

        }

     

        public class TpcRelationshipContext : DbContext

        {

            protected override void OnModelCreating(ModelBuilder modelBuilder)

            {

                modelBuilder.Entity<Order>();

                modelBuilder.Entity<Account>()

                    .Map(mc => { mc.MapInheritedProperties(); })

                    .ToTable("Accounts");

     

                modelBuilder.Entity<Contact>()

                    .Map(mc => { mc.MapInheritedProperties(); })

                    .ToTable("Contacts");

            }

        }

     

    Things to note about this are that first it doesn’t work yet due to some bugs and second even though EF is treating the BillToCustomerId property as an FK there isn’t a constraint for this in the database.  This is because the PK for this FK may be in one of two different tables depending on whether the relationship is to an Account or a Contact.

     

    I think that the main problem with the workaround you show is that the EF knows nothing about the FK properties or the navigation properties.  This means that all the internal logic that reasons about relationships for loading, fixup, creating update commands, and so on is not going to take these FKs or relationships into account.  This doesn’t mean that it won’t work, but there may be unexpected behavior because of it.

     

    Typically you can avoid exposing FKs in your model if you are willing to work with “independent associations”.  However, independent associations come with a lot of baggage and can make some things, such as working with N-Tier scenarios or doing concurrency resolution, very hard.  It’s usually best to use FK associations.  The IsIndependent methods in the relationship fluent API are used to setup a relationship as independent.

     

    Thanks,

    Arthur

    Wednesday, December 22, 2010 10:05 PM
    Moderator
  • Arthur,

    Thanks for your detailed explanations. Regarding your sample code now I assume that there will be some discriminant column in the Order table to identify the entity type (Account or Contact) that BillToCustomerId refers to and that  we will have a fluent API at our disposal to specify the relevant values. Can you confirm that this will be included in v1 of code-first?

    Regarding the default mapping convention I would argue that having two FKs intrinsically allows the encoding of the type of association without any other syntactic baggage (Discriminant)...

    Thanks,

     

     


    Dimitris Foukas
    Thursday, December 23, 2010 1:27 PM
  • Dimitris,

     

    There does not need to be a discriminator column.  If the FK in the Order table points to a PK in the Account table, then it refers to an Account.  If it points to a PK in the Contact table, then it refers to a Contact.  Or perhaps a better way to look at it is that the FK always points to a Customer and whether or not that Customer is an Account or a Contact is determined by whether it was obtained from the Account table or the Contact table.

     

    Thanks,

    Arthur

    Friday, December 24, 2010 2:26 AM
    Moderator
  • I see.

     

    Just because you can (since Accounts and Contacts are disjoint) it doesn't mean you should IMHO!

    Joins are involving both entities - if you have more subclasses of Customer then those should be included too...

    Would you do it this way if you were to manually build the data access layer?

     

    My 2 cents,

    Dimitris


    Dimitris Foukas
    Friday, December 24, 2010 7:51 AM