I have a class with two references to the same class. When updating the main class, I may also update the referenced class. When I have two references to the same (modified) object, I get an InvalidOperationException:
Attaching an entity of type 'ns.entity' failed because another entity of the same type already has the same primary key value. This can happen when using the 'Attach' method or setting the state of an entity to 'Unchanged' or 'Modified' if any entities in the graph have conflicting key values. This may be because some entities are new and have not yet received database-generated key values. In this case use the 'Add' method or the 'Added' entity state to track the graph and then set the state of non-new entities to 'Unchanged' or 'Modified' as appropriate.
Simple example:
public class Example {
public int OldestFriendId {get; set;}
public int BestFriendId {get; set;}
public virtual Friend Oldest {get; set; }
public virtual Friend Best {get; set; }
}
If while updating Example, I want to update the Middle name of my Oldest/Best friend, it works as long as they aren't the same. But if they are the same, then I get the above exception.
I can't figure out how to get this to work. I've tried setting references to null, saving them independently of the parent class, setting all references in them to null (EF is automatically creating two list of Examples in Friend).
How can I save an object that has changed when there are multiple references to it?
UPDATE: not yet working the way I want, but I have had some progress after removing the list of Examples from Friend. Also, the update is the result of a POST. Still investigating...
As sample code was asked for...this is from a post on a web app, no change was actually made
public ActionResult SaveEdit(int id, [Bind(Include = "OldestFriendId, BestFrinedId, Oldest, Best")] Example example)
{
if (ModelState.IsValid)
{
using (((WindowsIdentity)ControllerContext.HttpContext.User.Identity).Impersonate())
{
using (var _db = new exampleEntities())
{
//example.Best= example.Oldest; // this line would allow the update to work.
//next line is where the exception occurs
_db.Entry(example).State = EntityState.Modified;
_db.SaveChanges();
}
}
}
}
The EditorFor template:
#model Testing.Friend
<div class="col-md-10">
#Html.HiddenFor(model => model.FriendId)
#Html.EditorFor(model => model.FirstName)
#Html.EditorFor(model => model.LastName)
</div>
The Edit view for Example
#model Testing.Example
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>Example</h4>
<hr />
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
#Html.HiddenFor(model => model.ExampleId)
<div class="form-group">
#Html.LabelFor(model => model.OldestFriendId, "OldestFriendId", htmlAttributes: new { #class = "control-label col-md-2" })
#Html.HiddenFor(model => model.OldestFriendId)
#Html.EditorFor(model => model.Oldest)
</div>
<div class="form-group">
#Html.LabelFor(model => model.BestFriendId, "BestFriendId", htmlAttributes: new { #class = "control-label col-md-2" })
#Html.HiddenFor(model => model.BestFriendId)
#Html.EditorFor(model=> model.Best)
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
</div>
}
EDIT
The most likely cause is because when you retrieve the object back, it deserializes the 2 friends as 2 completely different objects (even when they are the same). Same problem as below, but rather than EF deserializing into 2 objects, ASP.NET MVC is doing it.
What you will have to do is something like the following:
Check if the 2 Friend ID's are the same (as ID is the PK). If not continue as normal
If they have the same ID, check if the 2 friend objects are the same.
If they are the same go to step 5.
Combine the changes together, however you want to deal with conflicts.
Set one of the Freinds to the same as the other Friend reference, e.g. Best = Oldest
SaveChanges()
Original Answer
My guess is that this is the classic problem of Include when you are retrieving the data.
When you do
Context.Examples.Include(x => x.Oldest).Include(x => x.Best).ToList()
What is happening is EF will create TWO objects of friend(Oldest and Best), even if they point to the same record. This is a known problem with include.
So when you go to save after update, EF sees them as 2 separate entities with the same key (and data) and complains.
If this is the case you have a couple of options:
Retrieve a list of all Friends for the current example and then the Example without the include
Let EF use LazyLoading and load the Friends when and as you need them.
My solution to the problem was to stop binding the whole object, and bind to the individual objects.
public ActionResult SaveEdit(int id, [Bind(Include = "OldestFriendId, BestFrinedId")] Example example,
Bind(Prefix="Oldest", Include = "FriendId, FirstName, MiddleName, LastName")] Friend oldest,
Bind(Prefix="Best", Include = "FriendId, FirstName, MiddleName, LastName")] Friend best) {
if (ModelState.IsValid)
{
using (((WindowsIdentity)ControllerContext.HttpContext.User.Identity).Impersonate())
{
using (var _db = new exampleEntities())
{
// do whatever processing you want on best and/or oldest
example.BestFriendId = best.FriendId;
example.OldestFriendId = oldest.FriendId;
_db.Entry(example).State = EntityState.Modified;
_db.SaveChanges();
}
}
}
}
EDIT: Replaced with full sample code
This example works for me.
I think is does what you are trying.
using System;
using System.Data.Common;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Migrations;
namespace Ef6Test {
public class Program {
public static void Main(string[] args) {
ExecDb1();
}
private static void ExecDb1() {
Database.SetInitializer(new MigrateDatabaseToLatestVersion<Ef6Ctx, Ef6MigConf>());
WhichDb.DbName = "MSAMPLEDB";
WhichDb.ConnType = ConnType.CtxViaDbConn;
var sqlConn = GetSqlConn4DBName(WhichDb.DbName);
var context = new Ef6Ctx(sqlConn);
context.Database.Initialize(true);
Console.WriteLine(WhichDb.DbName, context.Database.Exists() );
AddJunk(context);
}
public static class WhichDb {
public static string DbName { get; set; }
public static string ConnectionName { get; set; }
public static ConnType ConnType { get; set; }
}
public enum ConnType {
CtxViaDbConn,
CtxViaConnectionName
}
private static void AddJunk(DbContext context) {
var friend = new Friend();
friend.Name = "Fred";
friend.Phone = "555-1232424";
context.Set<Friend>().Add(friend);
context.SaveChanges();
// break here and check db content.
var eg = new Example();
eg.Best = friend; // set them equal
eg.Oldest = friend;
friend.Name = "Fredie"; // change the name of the fly
friend.Phone = "555-99999"; // and phone is also different
context.Set<Example>().Add(eg); Add the new example
context.SaveChanges();
// result... 2 records.
// The original friend record should be chnaged
}
public static DbConnection GetSqlConn4DBName(string dbName) {
var sqlConnFact = new SqlConnectionFactory(
"Data Source=localhost; Integrated Security=True; MultipleActiveResultSets=True");
var sqlConn = sqlConnFact.CreateConnection(dbName);
return sqlConn;
}
}
public class MigrationsContextFactory : IDbContextFactory<Ef6Ctx> {
public Ef6Ctx Create() {
switch (Program.WhichDb.ConnType) {
case Program.ConnType.CtxViaDbConn:
var sqlConn = Program.GetSqlConn4DBName(Program.WhichDb.DbName); //
return new Ef6Ctx(sqlConn);
case Program.ConnType.CtxViaConnectionName:
return new Ef6Ctx(Program.WhichDb.ConnectionName);
default:
throw new ArgumentOutOfRangeException();
}
}
}
public class Ef6MigConf : DbMigrationsConfiguration<Ef6Ctx> {
public Ef6MigConf() {
AutomaticMigrationsEnabled = true;
AutomaticMigrationDataLossAllowed = true;
}
}
public class Friend {
public int Id { get; set; }
public string Name { get; set; }
public string Phone { get; set; }
}
public class Example
{
public int Id { get; set; }
public int? BestFriendId { get; set; }
public int? OldestFriendId { get; set; }
public virtual Friend Best { get; set; }
public virtual Friend Oldest { get; set; }
}
public class Ef6Ctx : DbContext {
public Ef6Ctx(DbConnection dbConn) : base(dbConn, true) { }
public Ef6Ctx(string connectionName) : base(connectionName) { }
public DbSet<Friend> Friends { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Example>()
.HasOptional(t=>t.Best)
.WithMany()
.HasForeignKey(x=>x.BestFriendId);
modelBuilder.Entity<Example>()
.HasOptional(t => t.Oldest)
.WithMany()
.HasForeignKey(x => x.OldestFriendId);
}
}
}
Related
I am trying to display a checklist that gets data from MySQL Database and displays it in a view and updates the value of the variable (IsChecked) of each element in the table by whether we have checked the amenity or not (i am displaying some amenities). The model of the view is Hotel_5.ViewModel.BookingRoom, where BookingRoom is a custom model i created where i use multiple models. I get the exception at Model.AmenitiesList.Count(). The model is null.
This is my view
<div class="form-group">
#for (var i = 0; i < Model.AmenitiesList.Count(); i++)
{
#Html.CheckBoxFor(m => m.AmenitiesList[i].IsChecked, new { #class = "form-control" });
<label>#Model.AmenitiesList[i].amenityType</label>
//If you need to hide any values and get them in your post
#Html.HiddenFor(m => m.AmenitiesList[i].AmenityId)
#Html.HiddenFor(m => m.AmenitiesList[i].AmenityPrice)
}
</div>
This is my ViewModel
public class BookingRoom
{
public Bookings bookings { get; set; }
public Rooms rooms { get; set; }
public List<Amenities> AmenitiesList { get; set; } = new List<Amenities>();
}
This is my Amenities Model
public class Amenities
{
[Key]
public int AmenityId { get; set; }
public double AmenityPrice { get; set; }
public AmenityType amenityType { get; set; }
public bool IsChecked { get; set; }
}
public enum AmenityType
{
tv,
wi_fi,
hair_dryer,
help
}
When Querying you should Include its AmenitiesList too, otherwise it will be null:
In Controller:
var bookingRoom = context.BookingRooms.Include(b => b.AmenitiesList).FirstOrDefault(b => b.Id == someId);
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
please note that what I queried might not be what you want, it is just to demonstrate how to use Include() and also you should add using Microsoft.EntityFrameworkCore.
I'm having an issue with a problem that was supposed to be corrected in EFCore 2.1. I know this was not capable in 2.0. I have my project as a MVC Core 2.1 app with EFCore being at 2.1 also.
I have searched the web for several different ways of wording the problem and I get pieces of information that do not complete the entire code process required to make the code work correctly.
I have a model Mother and a mother has a sub model of Mailing Address and Physical Address. I am using a stored procedure with FromSql in order to give me the capability of having the multiple joins in my procedure. The stored procedure is almost impossible to write in Linq.
I will include everything I have coded that meets the examples I have already read. PLEASE NOTE: I am using Areas for my code because I am writing a system with multiple areas of functionality.
Models - Mother.cs
namespace Birth.Models
{
public class Mother
{
[Key]
public int MomId { get; set; }
public string MomFirst { get; set; }
public string MomLast { get; set; }
//[ForeignKey("MomId")]
public MotherAddress Physical { get; set; }
// public MotherAddress Mailing { get; set; }
}
//[Owned]
public class MotherAddress
{
public string pType { get; set; }
public string Street { get; set; }
public string PreDir { get; set; }
[Key]
public int MomId { get; set; }
public Mother Mother { get; set; }
}
}
Interface - IbirthRepository.cs
public interface IBirthRepository
{
IQueryable<Mother> Mothers { get; }
IQueryable<MotherAddress> MotherAddresses { get; }
}
Repository - BirthRepository.cs
public class BirthRepository : IBirthRepository
{
private BirthDbContext context;
public BirthRepository(BirthDbContext ctx)
{
context = ctx;
}
public IQueryable<Mother> Mothers => context.Mothers;
public IQueryable<MotherAddress> MotherAddresses => context.MotherAddresses;
}
Controller - GetMotherController.cs
namespace VSMaintenance.Areas.Birth.Controllers
{
[Area("Birth")]
[Route("Birth/GetMother")]
public class GetMotherController : Controller
{
private BirthDbContext context;
private IBirthRepository repository;
public GetMotherController(BirthDbContext ctx, IBirthRepository repo)
{
repository = repo;
context = ctx;
}
public IActionResult Load()
{
return View("Index");
}
[HttpPost]
public async Task<ActionResult> Retrieve(Mother mother)
{
var gbd = new GetBirthData(repository, context);
var mom = new Mother();
mom = await gbd.GetMotherData(mother.MomId);
return View("Mother", mom);
}
}
}
Main View - Mother.cshtml
#model Birth.Models.Mother
#{
ViewData["Title"] = "Mother";
}
<h2>Mother</h2>
#Html.DisplayNameFor(model => model.MomId)
#Html.DisplayFor(model => model.MomId)
<br />
#Html.DisplayNameFor(model => model.MomFirst)
#Html.DisplayFor(model => model.MomFirst)
<br />
#Html.DisplayNameFor(model => model.MomLast)
#Html.DisplayFor(model => model.MomLast)
<br /><br /><br />
#Html.RenderPartialAsync("Birth/_MotherAddress", Model.Physical)
#*#Html.RenderPartialAsync("Birth/_MotherAddress", Model.Mailing)*#
Partial View - MotherAddress.cshtml
#model Birth.Models.MotherAddress
<div class="container">
<div class="row">
<div class="col-sm-3">
#Html.DisplayNameFor(model => model.pType)
</div>
<div class="col-sm-3">
#Html.DisplayNameFor(model => model.Street)
</div>
<div class="col-sm-4">
#Html.DisplayNameFor(model => model.PreDir)
</div>
<div class="col-sm-2"> </div>
</div>
<div class="row">
<div class="col-sm-3">
#Html.TextBoxFor(model => model.pType, new { Style = "width:100%" })
</div>
<div class="col-sm-3">
#Html.TextBoxFor(model => model.Street, new { Style = "width:100%" })
</div>
<div class="col-sm-4">
#Html.TextBoxFor(model => model.PreDir, new { Style = "width:80%" })
</div>
<div class="col-sm-2"> </div>
</div>
</div>
Models.Data - GetBirthData.cs
namespace Birth.Models.Data
{
public class GetBirthData
{
private IBirthRepository repo;
pivate BirthDbContext _ctx;
public GetBirthData(IBirthRepository repository, BirthDbContext context)
{
repo = repository;
_ctx = context;
}
public async Task<Mother> GetMotherData(int MomId)
{
Mother list = new Mother();
try
{
string query = "exec MAINTGetMotherAddresses {0}";
list = await repo.Mothers
.FromSql(query, MomId)
.AsNoTracking()
.FirstOrDefaultAsync();
}
catch (Exception ex)
{
var msg = ex.Message;
}
return list;
}
}
}
DbContext - BirthDbContext.cs
namespace Birth.Models.Data
{
public class BirthDbContext : DbContext
{
public BirthDbContext(DbContextOptions<BirthDbContext> options)
:base(options) {
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Mother>(b =>
{
b.HasKey(e => e.MomId);
b.Property(e => e.MomId).ValueGeneratedNever();
//b.OwnsOne<MotherAddress>(e => e.Physical);
b.HasOne<MotherAddress>(e => e.Physical)
.WithOne(e => e.Mother)
.HasForeignKey<MotherAddress>(e => e.MomId);
b.ToTable("Table1");
});
}
public DbSet<Mother> Mothers { get; set; }
public DbSet<MotherAddress> MotherAddresses { get; set; }
}
}
SQLServer Stored Procedure - MAINTGetMotherAddresses
ALTER PROCEDURE MAINTGetMotherAddresses
#MotherId NUMERIC(18,0)
AS
BEGIN
SELECT
CAST(md.MOTHER_DETAIL_ID AS INT) AS MomId,
md.FIRST_NAME AS MomFirst,
md.LAST_NAME AS MomLast,
'Physical' AS pType,
maP.STREET_ADDRESS AS Street,--PhysicalStreet,
maP.PRE_DIRECTION AS PreDir --PhysicalPreDir--,
--'Mailing' AS MailingType,
--maM.STREET_ADDRESS AS MailingStreet,
--maM.PRE_DIRECTION AS MailingPreDir
,CAST(#MotherId AS INT) AS PhysicalMomId
--,CAST(#MotherId AS INT) AS MailingMomId
--,CAST(#MotherId AS INT) AS MotherAddressMomId
FROM dbo.MOTHER_DETAIL md
JOIN dbo.MOTHER_ADDRESS maP
ON maP.MOTHER_DETAIL_ID = md.MOTHER_DETAIL_ID
JOIN dbo.MOTHER_ADDRESS maM
ON maM.MOTHER_DETAIL_ID = md.MOTHER_DETAIL_ID
WHERE md.MOTHER_DETAIL_ID = #MotherId
AND maP.ADDRESS_TYPE in (133, 176)
AND maM.ADDRESS_TYPE IN (132, 175)
END
The Mother_Detail and Mother_Address tables have a lot more fields than I have listed here, so I'm not going to show the full table structure. If you create the tables and add an addresses to the Mother_Address table, being Address_Type of 176, then you will be able to see the result set I am trying to work with currently. My desire is to join the address a second time as seen in the stored procedure and return the mailing and physical in one resultset and have the system populate the MotherAddress model appropriately with both addresses.
Please help! This is very frustrating.
Randy
your stored procedure will get all cross joined data for 1 mom, here is correct sql:
SELECT m.Whatever, physical.Whatever, mailing.Whatever
FROM Mother m
CROSS APPLY
(
SELECT TOP 1 a.Name, a.Whatever
FROM Address a
WHERE a.momId= m.Id and a.pType in (1,2,3)
) physical
CROSS APPLY
(
SELECT TOP 1 b.Name, b.Whatever
FROM Address b
WHERE b.momId= m.Id and b.pType in (4,5,6)
) mailing
The reason your SP cannot map result to your models is: your SP returns a view which does not exist in your context. Current EF & Core do not give a good way to execute Query SP, but for inserting,updating,deleting only.
//call extension like this:
_context.QueryStoredProcedureAsync<YourViewModel>("YourProcedureName", ("a", 1), ("b", 2), ("c", 3));
//here is extension
public static class ContextExtensions
{
public static async Task<List<T>> QueryStoredProcedureAsync<T>(
this DbContext context,
string storedProcName,
params (string Key, object Value)[] args)//<==c# 7 new feature
{
using (var command = context.Database.GetDbConnection().CreateCommand())
{
command.CommandText = storedProcName;
command.CommandType = System.Data.CommandType.StoredProcedure;
foreach (var arg in args)
{
var param = command.CreateParameter();
param.ParameterName = arg.Key;
param.Value = arg.Value;
command.Parameters.Add(param);
}
using (var reader = await command.ExecuteReaderAsync())
{
return reader.MapToList<T>();
}
}
}
private static List<T> MapToList<T>(this DbDataReader dr)
{
var result = new List<T>();
var props = typeof(T).GetRuntimeProperties();
var colMapping = dr.GetColumnSchema()
.Where(x => props.Any(y => y.Name.ToLower() == x.ColumnName.ToLower()))
.ToDictionary(key => key.ColumnName.ToLower());
if (dr.HasRows)
{
while (dr.Read())
{
T obj = Activator.CreateInstance<T>();
foreach (var prop in props)
{
var val =
dr.GetValue(colMapping[prop.Name.ToLower()].ColumnOrdinal.Value);
prop.SetValue(obj, val == DBNull.Value ? null : val);
}
result.Add(obj);
}
}
return result;
}
}
you SP is just a query, I provide you 2 options to replace it:
use compile query: https://learn.microsoft.com/en-us/dotnet/framework/data/adonet/ef/language-reference/compiled-queries-linq-to-entities
use my simple sultion to remove SP, SP is not designed for query data, but for process data.
public partial class Mother
{
public Mother()
{
MotherAddresses = new HashSet<MotherAddress>();
}
[Key, Column("ID")]
public int Id { get; set; }
[StringLength(50)]
public string Name { get; set; }
[InverseProperty("Mother")]
public ICollection<MotherAddress> MotherAddresses { get; set; }
}
public partial class MotherAddress
{
[Key, Column(Order = 0)]
public int MotherId { get; set; }
[Key, Column(Order = 1)]
public int AddressType { get; set; }
[StringLength(50)]
public string Address { get; set; }
[ForeignKey("MotherId")]
[InverseProperty("MotherAddress")]
public Mother Mother { get; set; }
}
public enum AddressType
{
Physical,
Mailing,
}
public static class MotherExtension
{
public static MotherAddress MailingAddress(this Mother mom)
{
return mom.Address(AddressType.Mailing);
}
public static MotherAddress PhysicalAddress(this Mother mom)
{
return mom.Address(AddressType.Physical);
}
public static MotherAddress Address(this Mother mom, AddressType addressType)
{
return mom.MotherAddresses.FirstOrDefault(x => x.AddressType == addressType);
}
}
// here is in your DbContext
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<MotherAddress>(entity =>
{
entity.HasKey(e => new { e.MotherId, e.AddressType });
entity.HasOne(d => d.Mother)
.WithMany(p => p.MotherAddress)
.HasForeignKey(d => d.MotherId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_MotherAddress_Mother");
});
}
then in your html:
#Html.RenderPartialAsync("Birth/_MotherAddress", Model.PhysicalAddress())
#*#Html.RenderPartialAsync("Birth/_MotherAddress", Model.WhateverAddress())*#
I created two models of Class and Teacher, I want to configure many to many relationship using code first approach with two models.
When I add information into class and teacher tables, value not insert into TeacherClasses Table using many to many relationship Entity Framework code first approach.
Here code of models and controller below.
This is Class Model
public class Class
{
[Key]
public int ClassId { get; set; }
[Required]
public string ClassName { get; set; }
public virtual ICollection<Teacher> Teachers { get; set; }
}
This is Teacher Model
public class Teacher
{
[Key]
public int TeacherId { get; set; }
[Required]
public string TeacherName { get; set; }
[DataType(DataType.Date)]
public DateTime JoiningDate { get; set; }
public decimal Salary { get; set; }
public string Email { get; set; }
public virtual ICollection<Class> Classes { get; set; }
[NotMapped]
public virtual Class Class { get; set; }
[NotMapped]
public virtual ICollection<int> SelectedClassList { get; set; }
}
This is Teacher Controller
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "TeacherId,TeacherName,JoiningDate,Salary,Email,Classes,Class,SelectedClassList")] Teacher teacher)
{
if (ModelState.IsValid)
{
db.Teachers.Add(teacher);
db.SaveChanges();
teacher.Classes = new Collection<Class>();
foreach (var classId in teacher.SelectedClassList)
{
teacher.Classes.Add(new Class {ClassId = classId });
}
return RedirectToAction("Index");
}
return View(teacher);
}
this is image of database tables
in database class, teacher and teacherclasses tables shown
database
This is View of Teacher
<div class="form-group">
#Html.Label("Class List", htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#*#Html.ListBox("SelectedClassList", ViewData["ClassList"] as MultiSelectList)
#Html.ValidationMessageFor(model => model.Classes, "", new { #class = "text-danger" })*#
#Html.ListBoxFor(model => model.SelectedClassList, new MultiSelectList(ViewBag.ClassList, "value", "text"), new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.SelectedClassList, "", new { #class = "text-danger" })
</div>
</div>
From your description and the way you're saving the data there seem to be two possible scenarios you're trying to achieve:
Insert new teachers and connect them to existing classes.
Connect existing teachers to existing classes.
Whichever it is, the rule is always:
First attach existing entities to the context
Then add new entities
So for the first scenario that would be:
foreach (var classId in teacher.SelectedClassList)
{
var class = new Class { ClassId = classId }; // "stub" entity
db.Classes.Attach(class);
teacher.Classes.Add(class); // Assuming that teacher.Classes isn't null
}
db.Teachers.Add(teacher); // <= Add
db.SaveChanges();
And the second scenario:
db.Teachers.Attach(teacher); // <= Attach
foreach (var classId in teacher.SelectedClassList)
{
var class = new Class { ClassId = classId }; // "stub" entity
db.Classes.Attach(class);
teacher.Classes.Add(class); // Assuming that teacher.Classes isn't null
}
db.SaveChanges();
As you see, you never need to pull existing entities from the database. For creating associations all EF needs tho know is the ID values of the entities at both ends, so working with stub entities is the ideal approach here.
In the first scenario, if you don't attach the classes before adding the teacher, the classes will be marked as Added too, and you'll insert empty class records into the database (if they pass validation at all).
I have been searching all over the place, found many questions but neither of them answer my.
Thing I am trying to do is simple, let's say we have something like that:
public class Parent
{
public int ID { get; set; }
public List<Child> Children { get; set; }
}
public class Child
{
public int ID { get; set; }
public int ParentID { get; set; }
public Parent Parent { get; set; }
}
EF takes care of creating db and via scaffolding I get controllers and views. Everything works fine, I can create Parent. I can also create Child with ParentID chosen from drop down.
What I really want to do is remove drop down and pass ParentID as parameter like that:
public IActionResult Create(int id)
{
if (id == 0)
{
return NotFound();
}
ViewData["ID"] = id; //that a ParentID
return View();
}
And in view we got this:
<form asp-action="Create">
<div class="form-horizontal">
<h4>Child</h4>
<hr />
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="ParentID" value=#ViewData["ID"] />
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</div>
</div>
</form>
When submitting this I got "Cannot insert explicit value for identity column in table 'Children' when IDENTITY_INSERT is set to OFF." Now I looked all over the place and tried various attributes yet still I can't get this to work.
I will be grateful for any sort of help.
Update
Create method from controller:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("ID,ParentID")] Child child)
{
if (ModelState.IsValid)
{
_context.Add(child);
await _context.SaveChangesAsync();
return RedirectToAction("Index");
}
return View(book);
}
Just to show more about problem. Here I am using default scaffolded version with select control in view.
public IActionResult Create(int id)
{
if (id == 0)
{
return NotFound();
}
ViewData["ParentID"] = new SelectList(_context.Parents.Where(x => x.ID == 1), "ID", "ID"); //Works
ViewData["ParentID"] = new SelectList(_context.Parents.Where(x => x.ID == id), "ID", "ID"); // Doesn't work
return View();
}
After talking with people in comments I start to think that problem is caused by a bug in EF Core but I think I have found 'dirty' work around.
Here is Child model:
public class Child
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int ID { get; set; }
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int ParentID { get; set; }
[ForeignKey("ParentID")]
public Parent Parent { get; set; }
}
Now (for some reason) this solves "Cannot insert explicit value for identity column in table 'Children' when IDENTITY_INSERT is set to OFF." issue but creates new, we don't get unique id. I fixed that by making it myself, here is create method:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("ID,ParentID")] Child child)
{
if (ModelState.IsValid)
{
int _id;
do
{
_id = new Random().Next();
} while (await _context.Children.Where(b => b.ID == _id).AnyAsync());
child.ID = _id;
_context.Add(child);
await _context.SaveChangesAsync();
return RedirectToAction("Index");
}
ViewData["ID"] = id;
return View(child);
}
Now doWhile makes sure that we don't assign the same id to two different objects. Alternatives to Random option are to use Guid (when id is string) or GetHashCode (and increment in case of collision perhaps).
I am getting this error:
The UPDATE statement conflicted with the FOREIGN KEY constraint "FK_TitleMember". The conflict occurred in database "ParishDBSQL", table "dbo.Titles", column 'title_id'.
The statement has been terminated.
controller
Get Action
public ActionResult Edit(int? id)
{
var member = (from h in db.Members
join f in db.Titles on h.title_id equals f.title_id
where h.m_id == id
select new
{
title_id = h.title_id,
}).First();
var viewmodel = new MembersViewModel();
viewmodel.title_id = member.title_id;
ViewBag.Titles = new SelectList(db.Titles.ToList(), "title_id", "Titles", viewmodel.title_id);
return View(viewmodel);
}
Post Action
public ActionResult Edit(MembersViewModel vm)
{
if (ModelState.IsValid)
{
var member = db.Members.Find(vm.m_id);
member.title_id = vm.title_id;
ViewBag.Titles = new SelectList(db.Titles.ToList(), "title_id", "Titles", vm.title_id);
db.Members.Attach(member);
db.Entry(member).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(vm);
View
<div class="col-md-4">
<div class="form-group">
<label>Title</label>
#Html.DropDownList("Titles")
#Html.ValidationMessageFor(model => model.title_id)
</div>
</div>
Model
public partial class Title
{
public Title()
{
this.Heads = new HashSet<Head>();
this.Members = new HashSet<Member>();
}
public int title_id { get; set; }
public string Titles { get; set; }
public virtual ICollection<Head> Heads { get; set; }
public virtual ICollection<Member> Members { get; set; }
}
Viewmodel
public class MembersViewModel
{
public int m_id { get; set; }
public string titles { get; set; }
public int title_id { get; set; }
}
I want to update a title_id using a dropdownlist on my members table table but I get the error that is shown above.
i think that the error is in this line
member.title_id = vm.title_id
maybe vm.title_id it is not found in primary table and it could not be a foreign key. Check the correct value of this variable
if it doesn't solve your problem looking at your code I saw some thinghs that I didn't understand very well.
try to change your code in this way
if (ModelState.IsValid)
{
var member = db.Members.Find(vm.m_id);
member.title_id = vm.title_id;
ViewBag.Titles = new SelectList(db.Titles.ToList(), "title_id", "Titles", vm.title_id);
db.SaveChanges();
return RedirectToAction("Index");
}
Usually I use db.Entry(member).State = EntityState.Modified; if I have to do other changes before db.SaveChanges();
member is already attched to your table. Why do you db.Members.Attach(member);
update
change
#Html.DropDownListFor(m => m.title_id, ViewBag.Titles)
into
#Html.DropDownListFor(m => m.title_id, (SelectList) ViewBag.Titles)
because ViewBag is a dynamic object
The problem is the drop down list in the view. You really want to initialize it like this:
#Html.DropDownListFor(m => m.title_id, ViewBag.Titles)
The way you're initializing it now causes the selected value to be passed as a query string parameter - you should see the URL being posted to as ...?Titles=1 for example.