Alex
Alex

Reputation: 1557

Updating an Entity in EF with a 1:1 relationship

I have an application with 4 layers:

-Core           (Models)
-Repository     (EF DbContext actions)
-Service        (Business logic)
-Web            (MVC)

I'm trying to update an object with a 1:1 relationship with EF using the following method:

    public async Task<bool> UpdateProductTicketing(ProductTicketing ticketing)
    {
        var product = await GetProductByIdAsync(ticketing.ProductId);

        // Validation removed for simplicity

        // 'ticketing' passed validation so let's 
        // just replace it with the existing record.
        product.Ticketing = ticketing;


        _repo.ProductRepository.Update(product);
        return await _repo.SaveAsync();
    }

This works for an initial insert, but it doesn't work as I'd expect when I'm updating the record:

A first chance exception of type 'System.Data.Entity.Infrastructure.DbUpdateException' occurred...

The actual error message is:

Violation of PRIMARY KEY constraint 'PK_dbo.ProductTicketing'. Cannot insert duplicate key in object 'dbo.ProductTicketing'. The statement has been terminated.

Obviously the PK and FK "ProductId" doesn't change - so why does EF try to drop and insert my record instead of just updating it, and why does it fail?

But more importantly - how can I prevent this. I know I can manually map the object values and then update it - that works but it's tedious mapping two identical objects together and doesn't feel correct.

My repository for retrieving the Product object is in my Repository layer, while the method above is in my Service layer.

This is how I'm currently resolving this - and it looks as dirty as it feels:

    public async Task<bool> UpdateProductTicketing(ProductTicketing ticketing)
    {
        var product = await GetProductByIdAsync(ticketing.ProductId);

        // Validation removed for simplicity

        if (product.Ticketing == null)
        {
            product.Ticketing = ticketing;
        }
        else
        {
            product.Ticketing.AllowEventBooking = ticketing.AllowEventBooking;
            // Doing the same for all other properties etc
            // etc
            // etc
        }

        _repo.ProductRepository.Update(product);
        return await _repo.SaveAsync();
    }

How can I achieve this without doing all this horrible mapping an object to an identical object?

Edit

Here are the two models referred to above:

[Table(@"Products")]
public class Product
{
    [Key]
    public int Id { get; set; }
    public virtual ProductTicketing Ticketing { get; set; }
    // Removed others for clarity        

    [Timestamp]
    public byte[] RowVersion { get; set; }
}

[Table(@"ProductTicketing")]
public class ProductTicketing
{
    [Key, ForeignKey("Product")]
    public int ProductId { get; set; }

    public bool AllowEventBooking { get; set; }
    // Removed others for clarity

    public virtual Product Product { get; set; }
}

It's also probably worth noting that the "ProductTicketing" object I'm passing into the UpdateProductTicketing method is a new object created from values in my controller - but the ID is the same so I assume it should work.

Upvotes: 3

Views: 1176

Answers (2)

Mrchief
Mrchief

Reputation: 76198

I think I see the problem now - when you do product.Ticketing = ticketing;, EF treats this as a new insert.

To avoid this, you can do one of these things:

  1. Continue using the workaround (which is not a wokaround actually but just the way EF expects you to tell when to insert vs. when to update).

  2. Now this depends on rest of your code and design, but instead of fetching the product, you can fetch the ticket and update its properties. Of course, this means that if the ticketing is not found, you need to insert it which then kinda looks like what you're already doing with UpdateProductTicketing.

  3. Use the InsertOrUpdate pattern (I made some assumptions about your code but hopefully it gives you the idea - the main thing here is the InsertOrUpdate method):

    public class ProductRepository : IRepository 
    {    
        private SomeContext context;
    
        public void InsertOrUpdate(ProductTicketing ticketing) 
        { 
            context.Entry(ticketing).State = ticketing.ProductId == 0 ? 
                                           EntityState.Added : 
                                           EntityState.Modified; 
            context.SaveChanges(); 
        } 
    }
    
    
    // And a generic version
    public void InsertOrUpdate<T>(T entity) where T : class
    {
        if (context.Entry(entity).State == EntityState.Detached)
            context.Set<T>().Add(entity);
    
        context.SaveChanges(); 
    }
    

Upvotes: 4

korir
korir

Reputation: 51

You are getting that error because ef thinks that the ProductTicket is a new entity and is trying to insert the entity into the db. I don't know about the _repo.ProductRepository.Update(product) call but how about you attach the ProductTicket to the context and set the entity state to modified

Upvotes: 1

Related Questions