locked
Mapping exclusive-or relationship with EF code-first RRS feed

  • Question

  • An Order can be placed against either an individual (Contact) or a company (Account). Customer base expresses the concept of XOR relationship between Accounts and Contacts:

    Snippet

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

     

    We want Table Per Concrete class inheritance mapping for Customers:

    public class CustomerMap : EntityConfiguration<Customer><br/>
        {<br/>
          public CustomerMap()<br/>
          {<br/>
            MapHierarchy()<br/>
            .Case<Account>(c => new<br/>
            {<br/>
              AccountId = c.Id,<br/>
              Name = c.Name,<br/>
              Number = c.AccountNumber<br/>
            }).ToTable("dbo.Accounts");<br/>
    <br/>
            MapHierarchy()<br/>
            .Case<Contact>(c => new<br/>
            {<br/>
              ContactId = c.Id,<br/>
              Name = c.Name,<br/>
              c.FirstName,<br/>
              c.LastName<br/>
            }).ToTable("dbo.Contacts");<br/>
          }<br/>
        }
    
    Snippet

    Since mainstream DBs do not allow mapping the same FK to two or more tables we would like a kind of "conditional constraint" for mapping BillToCustomer. Orders will be related to both Accounts and Contacts but not at the same time :


    public class OrderMap : EntityConfiguration<Order>
        {
          public OrderMap()
          {   
            HasRequired<Customer> (o => o.BillToCustomer)
              .WithMany()
              .HasConstraint((o, c) => (o.BillToCustomer is Account) ? o.BillToAccountId == c.Id : o.BillToContactId == c.Id
              );
     
            MapSingleType(o => new
            {
              OrderId = o.Id,
              o.Name,
              o.BillToAccountId,
              o.BillToContactId 
            }).ToTable("dbo.Orders");
          }
        }
    

    Currently (CTP4) this is illegal. Is there any workaround to what I am trying to achieve? Note that I will be taking care of assigning unique GUIDs to customer hierarchies during construction.Ideally I would also like to avoid including the BillToAccountId & BillToContactId FKs in my Order domain model altogether using some kind of Snippet

    EntityMap.Related<Account> (a => a.Orders).Id
    

    in the mapping layer but then I guess conditional constraints will be hardly readable...

    Snippet

     

    Thursday, December 2, 2010 4:09 PM

Answers

  • Hi,

    XOR relationships are currently not supported in EF in general. You could model this as a single relationship if you used TPH or TPT mapping, unfortunately with TPC you would need to use separate relationships.

    XOR isn't a request I have heard before but you can suggest and vote on future features for EF on our User Voice site, this plays a significant role in prioritizing the features we choose to implement; https://data.uservoice.com/forums/72025-ado-net-entity-framework-ef-feature-suggestions

    ~Rowan

    • Marked as answer by dfoukas Friday, December 10, 2010 10:03 AM
    Friday, December 10, 2010 12:02 AM
    Moderator

All replies

  • Hi,

    This isn't supported in Code First, the best workaround I could suggest would be to model two separate relationships between Account->Order and Contact->Order.

    ~Rowan

    Saturday, December 4, 2010 9:13 PM
    Moderator
  • Hi Rowan,

    I am deeply disappointed - is this a limitation of the current code-first CTP or is XOR relationships of the kind I explained are not supported by EF?

    In any case, I urge you to reconsider because the kind of hierearchical relationships (xor & complete) and mapping (TPC) are the pillars of many excutable UML compilers (see for exmple Melllor's work - http://en.wikipedia.org/wiki/Executable_UML).  They also form the recommended OO translation pattern of Discriminating Union types you encounter in modern Functional Languages!

    Your workaround - separate navigation properties for AccountOrders & ContactOrders is not workable either. There are literally tens of navigation properties that I need to define in my domain for the Customer supertype (quotes, invoices, contracts, incidents, etc) and the API surface that this approach would expose to the clients would be un-necessarily complex...

    ~Dimitris 


    Dimitris Foukas, PhD CTO, Mantis Informatics S.A.
    Monday, December 6, 2010 11:00 AM
  • Hi,

    XOR relationships are currently not supported in EF in general. You could model this as a single relationship if you used TPH or TPT mapping, unfortunately with TPC you would need to use separate relationships.

    XOR isn't a request I have heard before but you can suggest and vote on future features for EF on our User Voice site, this plays a significant role in prioritizing the features we choose to implement; https://data.uservoice.com/forums/72025-ado-net-entity-framework-ef-feature-suggestions

    ~Rowan

    • Marked as answer by dfoukas Friday, December 10, 2010 10:03 AM
    Friday, December 10, 2010 12:02 AM
    Moderator
  • Hi Rowan,

     

    Thanks for your comments - XOR is a common UML facet for modelling disjoint and complete hierarchical relationships. It is also very common idiom in ER modeling approaches. I will definitely create a future request in User Voice - thanks for pointing this out.

     

     


    Dimitris Foukas, PhD CTO, Mantis Informatics S.A.
    Friday, December 10, 2010 10:03 AM
  • Rowan,

    Herer are the updated TPC mappings for the Account - Contact XOR relationships using CT5:

     

    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) // THIS DOESN'T WORK: o.BillToCustomer as Account
            .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] <br/>
        // NOTE: BillToAccountId and BillToContactId are disjoint & complete - validation rule to be inserted<br/>
        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 Customer in the db context?
    2. Is there any way to avoid exposing FKs in the Order model above?
    3. Is there any way to avoid the boilerplate Order model code for property setters?
    4. In general, what are the changes wrt to abstract base classes with navigation properties in CTP5?

     

    Regards,

    Dimitris

     


    Dimitris Foukas, PhD CTO, Mantis Informatics S.A.
    Monday, December 20, 2010 4:16 PM