Search code examples
asp.net-mvcentity-frameworkwcf

Entity Framework auto populating wrong entities


I'm developing the Music Store sample app with ASP.NET MVC, Entity Framework and WCF.

This is a layered application which has a common layer for the entities.

I have written a seed method which works fine, but when I try to add an Album to the Cart, another duplicate Album is also adding to the Album table plus duplicate Artists are also added. This happens when I add Album = album statement when populating the Cart object. I added this statement since when I create my view it generates null for item.Album.Title entry

This is my code:

public static void AddToCart(Album album, string ShoppingCartID)
{
            using (MusicStoreEntities db = new MusicStoreEntities())
            {
                // Get the matching cart and album instances
                var cartItem = db.Carts.SingleOrDefault(
                    c => c.CartId == ShoppingCartID
                         && c.AlbumId == album.AlbumId);

                if (cartItem == null)
                {
                    // Create a new cart item if no cart item exists
                    cartItem = new Cart
                    {
                        AlbumId = album.AlbumId,
                        CartId = ShoppingCartID,
                        Count = 1,
                        DateCreated = DateTime.Now,
                        Album = album
                    };

                    db.Carts.Add(cartItem);
                }
                else
                {
                    // If the item does exist in the cart, then add one to the quantity
                    cartItem.Count++;
                }

                // Save changes
                db.SaveChanges();
            }
}

Model classes:

namespace MusicStore.Core
{
    [Serializable]
    [DataContract]
    //[Bind(Exclude = "AlbumId")]
    public class Album
    {
        [DataMember]
        [ScaffoldColumn(false)]
        public int AlbumId { get; set; }

        [DataMember]
        [DisplayName("Genre")]
        public int GenreId { get; set; }

        [DataMember]
        [DisplayName("Artist")]
        public int ArtistId { get; set; }

        [DataMember]
        [Required(ErrorMessage = "An Album Title is required")]
        [StringLength(160)]
        public string Title { get; set; }

        [DataMember]
        [Required(ErrorMessage = "Price is required")]
        [Range(0.01, 100.00, ErrorMessage = "Price must be between 0.01 and 100.00")]
        public decimal Price { get; set; }

        [DataMember]
        [DisplayName("Album Art URL")]
        [StringLength(1024)]
        public string AlbumArtUrl { get; set; }

        [DataMember]
        public virtual Genre Genre { get; set; }

        [DataMember]
        public virtual Artist Artist { get; set; }
        public virtual List<OrderDetail> OrderDetails { get; set; }
    }
}

namespace MusicStore.Core
{
    [Serializable]
    [DataContract]
    public class Cart
    {
        [Key]
        [DataMember]
        public int RecordId { get; set; }
        [DataMember]
        public string CartId { get; set; }
        [DataMember]
        public int AlbumId { get; set; }
        [DataMember]
        public int Count { get; set; }
        [DataMember]
        public System.DateTime DateCreated { get; set; }
        [DataMember]
        public virtual Album Album { get; set; }
    }
}

Seed method:

namespace MusicStore.Data
{
    internal class CommonDBInitializer : CreateDatabaseIfNotExists<MusicStoreEntities>
    {
        protected override void Seed(MusicStoreEntities context)
        {
            var genres1 = new List<Genre>
            {
                new Genre
                {
                    Name = "Rock",
                    Description = "Rock",
                    Albums = new List<Album>()
                },
                new Genre
                {
                    Name = "Jazz",
                    Description = "Jazz",
                    Albums = new List<Album>()
                },
                new Genre
                {
                    Name = "Metal",
                    Description = "Metal",
                    Albums = new List<Album>()
                },
                new Genre
                {
                    Name = "Alternative",
                    Description = "Alternative",
                    Albums = new List<Album>()
                },
                new Genre
                {
                    Name = "Disco",
                    Description = "Disco",
                    Albums = new List<Album>()
                },
                new Genre
                {
                    Name = "Blues",
                    Description = "Blues",
                    Albums = new List<Album>()
                },
                new Genre
                {
                    Name = "Latin",
                    Description = "Latin",
                    Albums = new List<Album>()
                },
                new Genre
                {
                    Name = "Reggae",
                    Description = "Reggae",
                    Albums = new List<Album>()
                },
                new Genre
                {
                    Name = "Pop",
                    Description = "Pop",
                    Albums = new List<Album>()
                },
                new Genre
                {
                    Name = "Classical",
                    Description = "Classical",
                    Albums = new List<Album>()
                },
            };
            genres1.ForEach(d => context.Genres.Add(d));
            context.SaveChanges();

            var artist1 = new List<Artist>
            {
                new Artist
                {
                    Name = "Aaron Copland & London Symphony Orchestra",

                },
                 new Artist
                {
                    Name = "Barry Wordsworth & BBC Concert Orchestra",

                },

            };

            artist1.ForEach(d => context.Artists.Add(d));
            context.SaveChanges();

            var album1 = new List<Album>
            {
                new Album { Title = "The Best Of Men At Work",  Price = 8.99M,  AlbumArtUrl = "/Content/Images/placeholder.gif",Genre = genres1.FirstOrDefault(d => d.GenreId == 1), Artist  = artist1.FirstOrDefault(d => d.ArtistId == 1) },
               new Album { Title = "A Copland Celebration, Vol. I",  Price = 8.99M, AlbumArtUrl = "/Content/Images/placeholder.gif",Genre = genres1.FirstOrDefault(d => d.GenreId == 2),Artist  = artist1.FirstOrDefault(d => d.ArtistId == 2) },

            };

            album1.ForEach(s => context.Albums.Add(s));

            context.SaveChanges();

        }
    }

Controller:

namespace MusicStore.Web.Controllers
{
    public class ShoppingCartController : Controller
    {
        MusicShoppingCartMgr.Cart serviceref1 = new MusicShoppingCartMgr.Cart();
        MusicShoppingCartMgr.iShoppingCart servicemethodref1 = new iShoppingCartClient();
        //
        // GET: /ShoppingCart/
        public ActionResult Index()
        {
            var cart = ShoppingCart.GetCart(this.HttpContext);

            // Set up our ViewModel
            var viewModel = new ShoppingCartViewModel
            {
                CartItems = cart.GetCartItems(cart.ShoppingCartId),
                CartTotal = cart.GetTotal(cart.ShoppingCartId)
            };

            // Return the view
            return View(viewModel);
        }
        //
        // GET: /Store/AddToCart/5

        public ActionResult AddToCart(int id)
        {
            var addedAlbum = servicemethodref1.GetAlbum(id);

            // Add it to the shopping cart
            var cart = ShoppingCart.GetCart(this.HttpContext);

            cart.AddToCart(addedAlbum, cart.ShoppingCartId);

            // Go back to the main store page for more shopping
            return RedirectToAction("Index");
        }

    }
}

View:

@model MusicStore.Web.ViewModels.ShoppingCartViewModel
@{
    ViewBag.Title = "Shopping Cart";
}
<script src="/Scripts/jquery-1.10.2.min.js" type="text/javascript"></script>

<h3>
    <em>Review</em> your cart:
</h3>
<p class="button">
    @Html.ActionLink("Checkout >>", "AddressAndPayment", "Checkout")
</p>
<div id="update-message">
</div>
<table>
    <tr>
        <th>
            Album Name
        </th>
        <th>
            Price (each)
        </th>
        <th>
            Quantity
        </th>
        <th></th>
    </tr>
    @foreach (var item in Model.CartItems)
    {
        <tr id="[email protected]">
            <td>
                @Html.ActionLink(item.Album.Title, "Details", "Store", new { id = item.AlbumId }, null)
            </td>
            <td>
                @item.Album.Price
            </td>
            <td id="[email protected]">
                @item.Count
            </td>
            <td>
                <a href="#" class="RemoveLink" data-id="@item.RecordId">Remove from cart</a>
            </td>
        </tr>
    }
    <tr>
        <td>
            Total
        </td>
        <td></td>
        <td></td>
        <td id="cart-total">
            @Model.CartTotal
        </td>
    </tr>
</table>

Question is: why it is getting auto populate as such unusual way, and how to fix it?


Solution

  • You have the duplication because into your AddToCart method, you instantiated a new context MusicStoreEntities. That context doesn't know anything about the Album entity you pass through album parameter.

    So when you did this code:

    cartItem = new Cart
    {
        // [...]
        Album = album
    };
    

    and called db.Carts.Add(cartItem);, the cartItem will be on Added state. EF will also check the cartItem object graph and found that album was not tracked by the new context (remember this instance came from your method's parameter) and then by default it will also put that entity on Added state. EF will do the same with album object graph and you will get the artist of that album on Added state too. When calling db.SaveChanges();, each entity with Added state will be considered as a generate a new insertion into the database so that is why you've duplication.

    To fix the problem, you can call db.Attach(album) and let the context take care about the entity like below:

    using (MusicStoreEntities db = new MusicStoreEntities())
    {
         db.Albums.Attach(album);
    
        // [...]
    }
    

    Or intead of using the album instance, you can just set the foreign key property AlbumId and get rid of Album property assignation. So you'll just have this when creating a new cart item:

    cartItem = new Cart
    {
        AlbumId = album.AlbumId,
        CartId = ShoppingCartID,
        Count = 1,
        DateCreated = DateTime.Now
    };