MVC 4 Add a child record to existing parent - entity-framework

How do you add a child to a parent record in MVC 4 using EF?
I've got a grid showing News. I need to add Updates that are children records of those. One DB table has News, another has Updates. They are linked by NewsID in each table.
NEWS MODEL
[Key]
[DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
public int NewsId { get; set; }
public string Body { get; set; }
//other fields
UPDATES MODEL
[Key]
[DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
public int NewsUpdateId { get; set; }
public int NewsId { get; set; }
public string Body { get; set; }
//other fields
NEWS INDEX VIEW (only partly posted here; it's a grid showing these detail rows)
#foreach (var item in Model) {
<tr>
<td>#Html.ActionLink(item.Headline, "Details", new { id=item.NewsId })</td>
<td>#Html.Raw(item.Body.Remove(300))...</td>
//other fields
<td>#Html.ActionLink("Edit", "Edit", new { id=item.NewsId })</td>
//WOULD LIKE ANOTHER LINK GOING TO "UPDATE" SCREEN TO ADD CHILD UPDATES, PASSING IN THE PARENT ID
</tr>
}
CONTROLLER (right now)
public ActionResult Update()
{
return View();
}
But of course when I'm on that Update screen and save, there's no NewsID to save into Updates and it fails. How can I pass NewsID from the index screen/grid into my "Create Update" screen, then later retrieve it while on that Update screen and add it to my save action?
UPDATE
Since no one has any idea how to do this, I'm making it up as I go along without luck.
I've tried using the NewsUpdate screen like an Edit where you pass the id value. The difference is that, for Edit, you're editing that record. For this NewsUpdate, I'm adding a NewsUpdate, which is a child record of News, so I have the existing NewsID and need to pass it to my NewsUpdate screen and ultimately use it to set NewsUpdate.NewsID to it.
When I save the NewsUpdate screen, the model is invalid because it is missing, so I tried to this to set it:
var NewsID = Request.Url.PathAndQuery.ToString(); //grab it from url News/Update/8
NewsID = NewsID.Replace("/News/Update/", ""); //results in NewsID = 8
NewsUpdate.NewsID = Convert.ToInt32(NewsID);
This works, but the model is still invalid right after this. So this is no good.

Got it.
public ActionResult Update(int id = 0)
{
NewsUpdate myUpdate= db.NewsUpdate.Create(); //create empty child model
myUpdate.NewsID = id; //set my parent's ID on the child model
View(myUpdate); //send child model to child's create screen aka "NewsUpdate"
}
This passes my parent id in the child model and the rest of the NewsUpdate screen, which is like a standard Create screen, just functions as usual.

Related

Cannot insert explicit value for identity column in table when IDENTITY_INSERT is set to OFF - EF Core many-to-many (child) data

Following through Julie Lerman's Pluralsight course EF Core 6 Fundamentals I've created two classes in my own project (my own design, but identical to the course in terms of class structure/data hierarchy):
Class 1: Events - To hold information about an event being held (e.g. a training course), with a title and description (some fields removed for brevity):
public class EventItem
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int EventItemId { get; set; }
[Required(AllowEmptyStrings = false)]
public string EventTitle { get; set; }
public string? EventDescription { get; set; }
[Required]
public List<EventCategory> EventCategories { get; set; } = new();
}
Class 2: Event categories - Each event can be linked to one or more pre-existing (seeded) categories (e.g. kids, adult).
public class EventCategory
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int EventCategoryId { get; set; }
[Required]
public string EventCategoryName { get; set; }
public List<EventItem>? EventItems { get; set; }
}
In my Razor form to create the event, the user can select from multiple categories. Using EF Core I take the posted data (via a VM/DTO object) and construct the relevant parent/child entities. However upon saving to the database I get an exception as EF Core tries to re-create the categories when they already exist:
Cannot insert explicit value for identity column in table
'EventCategories' when IDENTITY_INSERT is set to OFF.
My code explicitly looks up the existing categories selected by the user, but the context tracker appears to still believe they need inserting, in addition to creating the many-to-many relationship.
I'd appreciate any input as to why this is happening please:
using (var dbcontext = DbFactory.CreateDbContext())
{
// Get selected categories from user's check box list
var selectedCategoryIds = _eventCagetories.Where(c => c.isSelected).Select(c => c.EventCategoryId).ToList();
// Create new Event
var newEventItem = new EventFinderDomain.Models.EventItem() {
EventTitle = _eventItemDTO.EventTitle,
EventDescription = _eventItemDTO.EventDescription,
EventUrl = _eventItemDTO.EventUrl,
TicketUrl = _eventItemDTO.TicketUrl
};
// Find categories from the database based on their ID value
var selectedEventCategories = dbcontext.EventCategories.Where(c => selectedCategoryIds.Contains(c.EventCategoryId)).ToList();
// Add the categories to the event
newEventItem.EventCategories!.AddRange(selectedEventCategories);
// Add the event to the change tracker
await dbcontext.EventItems.AddAsync(newEventItem); // <-- Created correctly with child list objects added
// Detect changes for debugging
dbcontext.ChangeTracker.DetectChanges();
var debugView = dbcontext.ChangeTracker.DebugView; // <-- Incorrectly shows newEventItem.Categories being added
// Save to database
await dbcontext.SaveChangesAsync(); // <-- Cannot insert explicit value for identity column
}
The Event entity appears to be correctly created in the debugger with its related child categories included:
The change tracker however incorrectly shows the selected categories being added again when they already exist:
After commenting out every line of code in the app and adding back in until it broke, it emerges the problem was elsewhere within Program.cs:
builder.Services.AddDbContextFactory<EventFinderContext>(
opt => opt.UseSqlServer(new SqlConnectionStringBuilder() {/*...*/}.ConnectionString)
.EnableSensitiveDataLogging()
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking) // <-- THE CULPRIT
);
In the training video this method was described as a way of reducing overhead for disconnected apps. I had assumed that because of the disconnected nature of HTTP, this would be beneficial and that context would be re-established when creating the model's child data. This was incorrect on my part.
I should have used .AsNoTracking() only when retriving read-only data from my database. For example, loading in the child-data for a new model that wouldn't be modified directly, but used to create the many-to-many data (explicitly, for the category data option items only and not for the event data).

Cannot insert explicit value for identity column - into related table

I have a database first model.
My application UI provides a group of checkboxes, one for each value in Data_Type.
When the user checks one, I expect a row to be added in BUS_APPL_DATA_TYPE,
however I'm getting an error about Cannot insert explicit value for identity column in DATA_TYPE (And I absolutely do not actually want to insert data in this table)
My EF Model class for BUS_APPL has this property
public ICollection<BusApplDataType> BusApplDataType { get; set; }
And that EF Model class looks like
public partial class BusApplDataType
{
public int BusApplId { get; set; }
public int DataTypeId { get; set; }
[Newtonsoft.Json.JsonIgnore]
public BusAppl BusAppl { get; set; }
public DataType DataType { get; set; }
}
What exactly do I need to add to the BusApplDataType collection to get a record to be inserted in BUS_APPL_DATA_TYPE?
Edit:
At a breakpoint right before SaveChanges.
The item at index 2 is an existing one and causes no issues.
The item at index 3 is new. Without this everything updates fine. There is a DATA_TYPE with id 5 in the database.
The surrounding code, if it helps.
[HttpPut("{id}")]
public IActionResult Update(int id, [FromBody] BusAppl item)
{
...
var existing = _context.BusAppl.FirstOrDefault(t => t.Id == id);
...
existing.BusApplDataType = item.BusApplDataType; //A bunch of lines like this, only this one causes any issue.
...
_context.BusAppl.Update(existing);
_context.SaveChanges();
return new NoContentResult();
}
My issue was that I needed to use my context to look up the actual entity, using info passed, instead of using the one with all the same values that was passed into my api directly.

EF many-to-many relationship and data duplication

I have a trouble with EF (6.1.3)
I have created next classes (with many-to-many relationship):
public class Record
{
[Key]
public int RecordId { get; set; }
[Required]
public string Text { get; set; }
public virtual ICollection<Tag> Tags { get; set; }
}
public class Tag
{
[Key]
public int TagId { get; set; }
[Required]
public string Name { get; set; }
public virtual ICollection<Record> Records{ get; set; }
}
And method:
void AddTags()
{
Record[] records;
Tag[] tags;
using (var context = new AppDbContext())
{
records = context.Records.ToArray();
}//remove line to fix
tags = Enumerable.Range(0, 5).Select(x => new Tag()
{
Name = string.Format("Tag_{0}", x),
Records= records.Skip(x * 5).Take(5).ToArray()
}).ToArray();
using (var context = new AppDbContext()){ //remove line to fix
context.Tags.AddRange(tags);
context.SaveChanges();
}
}
If I use two contexts, the records (which were added to created tags) will be duplicated. If I remove marked rows - problem disappears.
Is there any way to fix this problem without using the same context?
If you can, better reload entities or not detach them at all. Using multiple context instances in application is overall making things much more complicated.
The problem for you comes from the Entity Framework entity change tracker. When you load entitites from your DbContext and dispose that context, entities get detached from entity change tracker, and Entity Framework has no knowledge of any changes made to it.
After you reference detached entity by an attached entity, it (detached entity) immediately gets into entity change tracker, and it has no idea that this entity was loaded before. To give Entity Framework an idea that this detached entity comes from the database, you have to reattach it:
foreach (var record in records) {
dbContext.Entry(record).State = EntityState.Unchanged;
}
This way you will be able to use records to reference in other objects, but if you have any changes made to these records, then all these changes will go away. To make changes apply to database you have to change state to Added:
dbContext.Entry(record).State = EntityState.Modified;
Entity Framework uses your mappings to determine row in database to apply changes to, specifically using your Primary Key settings.
A couple examples:
public class Bird
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
public string Color { get; set; }
}
public class Tree
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
}
public class BirdOnATree
{
[Column(Order = 0), Key, ForeignKey("Bird")]
public int BirdId { get; set; }
public Bird Bird { get; set; }
[Column(Order = 1), Key, ForeignKey("Tree")]
public int TreeId { get; set; }
public Tree Tree { get; set; }
public DateTime SittingSessionStartedAt { get; set; }
}
Here's a small entity structure so that you could see how it works. You can see that Bird and Tree have simple Key - Id. BirdOnATree is a many-to-many table for Bird-Tree pair with additional column SittingSessionStartedAt.
Here's the code for multiple contexts:
Bird bird;
using (var context = new TestDbContext())
{
bird = context.Birds.First();
}
using (var context = new TestDbContext())
{
var tree = context.Trees.First();
var newBirdOnAtree = context.BirdsOnTrees.Create();
newBirdOnAtree.Bird = bird;
newBirdOnAtree.Tree = tree;
newBirdOnAtree.SittingSessionStartedAt = DateTime.UtcNow;
context.BirdsOnTrees.Add(newBirdOnAtree);
context.SaveChanges();
}
In this case, bird was detached from the DB and not attached again. Entity Framework will account this entity as a new entity, which never existed in DB, even though Id property is set to point to existing row to database. To change this you just add this line to second DbContext right in the beginning:
context.Entry(bird).State = EntityState.Unchanged;
If this code is executed, it will not create new Bird entity in DB, but use existing instead.
Second example: instead of getting bird from the database, we create it by ourselves:
bird = new Bird
{
Id = 1,
Name = "Nightingale",
Color = "Gray"
}; // these data are different in DB
When executed, this code will also not create another bird entity, will make a reference to bird with Id = 1 in BirdOnATree table, and will not update bird entity with Id = 1. In fact you can put any data here, just use correct Id.
If we change our code here to make this detached entity update existing row in DB:
context.Entry(bird).State = EntityState.Modified;
This way, correct data will be inserted to table BirdOnATree, but also row with Id = 1 will be updated in table Bird to fit the data you provided in the application.
You can check this article about object state tracking:
https://msdn.microsoft.com/en-US/library/dd456848(v=vs.100).aspx
Overall, if you can avoid this, don't use object state tracking and related code. It might come to unwanted changes that are hard to find source for - fields are updated for entity when you don't expect them to, or are not updated when you expect it.

Entity Framework identity column always zero

I'm using following class to insert products to database.
ID column is primary key.
After adding multiple products to db context (without calling savechanges method) all newly added rows identity columns are zero!
My scene...
User adds several products and browse them on the data grid.
User selects one product and adds some barcodes to selected product.
When user finishes the job clicks on save button and application calls SaveChanges method!
When user wants to add some barcodes to products firstly I need to find selected product from context and adds entered barcode text to Barcodes list. But I cant do that because all products identity columns value are the same and they are zero.
How can I solve this problem?
public class Product
{
public int ProductID { get; set; }
public string Code { get; set; }
public string Name { get; set; }
public virtual List<Barcode> Barcodes { get; set; }
}
public class Barcode
{
public int BarcodeID { get; set; }
public string BarcodeText { get; set; }
public int ProductID { get; set; }
public virtual Product Product { get; set; }
}
Identity column value is assigned by database when you are inserting record into table. Before you call SaveChanges no queries are executed, nothing is passed to database and back. Context just keeps in-memory collection of entities with appropriate state (state defines which time of query should be executed during changes saving - for new entities, which have Added state, insert query should be generated and executed). So, ID stays with its default value, which is zero for integer. You should not give value manually. After inserting entities into database, context will receive ID value and update entity.
UPDATE: When you are adding Barcode to existing product, then EF is smart enough to update keys and foreign keys of entities:
var product = db.Products.First(); // some product from database
var barcode = new Barcode { BarcodeText = "000" };
// at this point barcode.ID and barcode.ProductID are zeros
product.Barcodes.Add(barcode);
db.SaveChanges(); // execute insert query
// at this point both PK and FK properties will be updated by EF

Entity Framework / MVC Remove Item from Collection

What are some ways I can delete an item from a collection? (I am using MVC 4 and EF.)
As an example:
public class Birthday
{
public string Name { get; set; }
public virtual ICollection<Gift> Gifts { get; set; }
}
public class Gift
{
public string Name { get; set; }
public double Price { get; set; }
}
I'm using Editing a variable length list, ASP.NET MVC 2-style to create a dynamic list of Gifts.
The example is shows how to "Delete" a row. This will delete the row from the page and the correct Gifts are sent to the controller.
When I update the Birthday / Gifts everything new is updated properly, but anything deleted is still there.
So my question is what are some preferred ways to remove Gifts?
Two ways I've thought of already:
Get a Birthday from the DB and compare the Gifts removing as needed. I don't love this idea because it seems heavy handed.
Use WebApi / Ajax and delete the Gift from the list and the DB when the user pushes the delete link. I like this better than #1 but does this put too much business logic in the presentation layer?
I'm guessing that other people have had this similar problem and have a clever solution I haven't thought of yet.
Thanks in advance!
Make a Gifts api controller.
Let it have a Delete method accepting an Id of whatever type your Id is.
And do something like this in it:
public class GiftsController: ApiController
{
public void Delete(Guid Id)
{
var context = new MyContext();
var giftToDelete = context.Gifts.FirstOrDefault(g=> g.Id == Id);
if(giftToDelete != null)
{
context.Gifts.Remove(giftToDelete);
context.SaveChanges();
}
}
}
Make sure you make a DELETE request to this api in your JS delete function.
You may also replace the body of this method with some Service.DeleteGift(Id) if you're too concerned about doing things in the right place.
Like this:
public class ValuesController : ApiController
{
private List<string> list = new List<string>{"Item1","Item2","Item3","Item4","Item5"};
// DELETE api/values/5
public List<string> DeleteItem(int id)
{
list.Remove(list.Find((i => i.ToString().Contains(id.ToString()))));
return list;
}
}