Entity Framework Code First Fluent API configuration for one to one identifying relationship

c# code-first ef-fluent-api entity-framework entity-framework-6

Question

I have the following class structure:

enter image description here

How to configure Fluent API to put identifying relationship in Cards table?

I mean

  • Cards Table PK: Id, CustomerId
  • Cards Table FK: CustomerId

I would like the previous Card be deleted when I assign a new one to Customer.Card property.

So I defined my classes this way:

public class Customer
{
    public int Id { get; private set; }
    public virtual Card Card { get; set; }
}

public abstract class Card
{
    public int Id { get; private set; }
}

public class Visa : Card
{
}

public class Amex : Card
{
}

DbContext looks like this:

public class Context : DbContext
{
    public DbSet<Customer> Customers { get; set; }
    public DbSet<Card> Cards { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<Customer>()
            .HasRequired(c => c.Card)
            .WithRequiredPrincipal()
            .Map(a => a.MapKey("CustomerId"))
            .WillCascadeOnDelete();

        modelBuilder.Entity<Card>();
    }
}

Here is the test:

[TestClass]
public class UnitTest1
{
    [TestMethod]
    public void TestMethod1()
    {
        var context = new Context();
        var customer = new Customer();
        context.Customers.Add(customer);
        customer.Card = new Visa();
        context.SaveChanges();

        customer.Card = new Amex();
        context.SaveChanges();

        Assert.AreEqual(1, context.Customers.Count());
        Assert.AreEqual(1, context.Cards.Count());
    }
}

It does not work at all. I have this on second save and I do not know how to specify identifying relationship here:

Unhandled Exception: System.Data.Entity.Infrastructure.DbUpdateException: An err or occurred while saving entities that do not expose foreign key properties for their relationships. The EntityEntries property will return null because a singl e entity cannot be identified as the source of the exception. Handling of except ions while saving can be made easier by exposing foreign key properties in your entity types. See the InnerException for details. ---> System.Data.Entity.Core.U pdateException: A relationship from the 'Customer_Card' AssociationSet is in the 'Deleted' state. Given multiplicity constraints, a corresponding 'Customer_Card _Target' must also in the 'Deleted' state.

UPDATE It's easy to make it work for one-to-many relationships. You can find a full example below:

[TestClass]
public class UnitTest1
{
    [TestMethod]
    public void TestMethod1()
    {
        var context = new Context();
        var customer = new Customer();
        context.Customers.Add(customer);
        customer.Cards.Add(new Visa());
        context.SaveChanges();

        customer.Cards[0] = new Amex();
        context.SaveChanges();

        Assert.AreEqual(1, context.Cards.Count());
    }
}

public class Customer
{
    public Customer()
    {
        Cards = new List<Card>();
    }

    public int Id { get; private set; }
    public virtual List<Card> Cards { get; set; }
}

public abstract class Card
{
    public int Id { get; private set; }
    public int CustomerId { get; private set; }
}

public class Visa : Card
{
}

public class Amex : Card
{
}

public class Context : DbContext
{
    static Context()
    {
        Database.SetInitializer(new DropCreateDatabaseAlways<Context>());
    }

    public DbSet<Customer> Customers { get; set; }
    public DbSet<Card> Cards { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<Customer>()
            .HasMany(c => c.Cards)
            .WithRequired()
            .HasForeignKey(c => c.CustomerId)
            .WillCascadeOnDelete();

        modelBuilder.Entity<Card>()
            .HasKey(c => new { c.Id, c.CustomerId });
    }
}
1
4
1/18/2015 10:28:06 PM

Accepted Answer

The way EF implements one-to-one is to make the dependent entity have a primary key that's also a foreign key to the principle entity. So the dependent's PK is naturally constrained to existing principle PK values.

So using your classes, slightly modified:

public class Customer
{
    public int CustomerId { get; private set; }
    public virtual Card Card { get; set; }
}

public abstract class Card
{
    public int CustomerId { get; private set; }
}

public class Visa : Card { }

public class Amex : Card { }

And the mapping:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    modelBuilder.Entity<Customer>().HasRequired(c => c.Card)
                                   .WithRequiredPrincipal();
    modelBuilder.Entity<Card>().HasKey(c => c.CustomerId);
}

So Card only has CustomerId as PK and FK, not two separate fields.

BUT

Trying this, I found out that there's a bug in EF (6.1.2). This is what I did:

using (var db = new TempModelsContext())
{
    var cst = new Customer { Name = "Customer1", 
                             Card = new Amex { Number = "Amex" } };
    db.Customers.Add(cst);
    db.SaveChanges();
}

using (var db = new TempModelsContext())
{
    var cst = db.Customers.Include(c => c.Card).Single(c => c.CustomerId == 1);
    cst.Card = new Visa { Number = "Visa" };
    db.SaveChanges();
}

(Added Name and Number for convenience).

Normally this would be OK. EF is smart enough to see that a 1:1 dependent entity is replaced and it just updates the Number field (effectively deleting the old card).

But EF overlooks the inheritance (for which I used the default, TPH). Of course it should also update the discriminator field, but it doesn't. You end up with an Amex card, having "Visa" as number if you re-fetch the items from the database.

So, sadly, even with this model, you first have to remove the old card, and then add the new one:

var cst = db.Customers.Include(c => c.Card).Single(c => c.CustomerId == 1);
db.Cards.Remove(cst.Card);
db.SaveChanges();

cst.Card = new Visa { Number = "Visa" };
db.SaveChanges();

This is clumsy enough, not to mention that you'd also want to wrap this in a TransactionScope.

3
1/18/2015 10:57:45 PM

Popular Answer

Entity Framework doesn't really allow this sort of an operation. You can't "delete" an object from the database simply by trying to replace it with another object. even with Cascade Delete, you still have to issue a delete command in Entity Framework, else you end up with an orphaned item in your context. You can try to override the SaveChanges() method to trap this behavior, but it won't be an easy patch.

your best bet would be to check if a card exists, and if so remove it before adding the new card. this can easily be wrapped up into a repeatable function call, like so:

public void AddCard(Customer customer, Card card, Context context)
{
    if (customer.Card != null)
    {
        context.Cards.Remove(customer.Card);
    }
    customer.Card = card;
}

Edit

To be more clear, Entity Framework cannot batch a deletion of a relational object and the addition of a replacement object into the same SaveChanges() call.

This works fine:

Customer.Card = null;
SaveChanges();
Customer.Card = new Amex();
SaveChanges();

Note the multiple calls to SaveChanges(). The function provided earlier is more a wrapper function to avoid the extra SaveChanges() call.



Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow