I am having a really hard time updating and deleting many to many relationships with EF Code-first.
I have a fairly simple Model:
public class Issue
{
[Key]
public int IssueId { get; set; }
public int Number { get; set; }
public string Title { get; set; }
public DateTime Date { get; set; }
public virtual ICollection<Creator> Creators { get; set; }
}
public class Creator
{
[Key]
public int CreatorId { get; set; }
public string FirstName { get; set; }
public string MiddleName { get; set; }
public string LastName { get; set; }
public virtual ICollection<Issue> Issues { get; set; }
}
public class Icbd : DbContext
{
public DbSet<Issue> Issues { get; set; }
public DbSet<Creator> Creators { get; set; }
}
I have been unable to figure out how to update the many-to-many realtionship using the EF context. In my isnert/update action, I have this code:
[HttpPost]
public ActionResult EditIssue ( Issue issue, int[] CreatorIds )
{
if(CreatorIds == null)
ModelState.AddModelError("CreatorIds", "Please specify at least one creator");
if(ModelState.IsValid)
{
// insert or update the issue record (no relationship)
if (issue.IssueId == 0)
db.Issues.Add(issue);
else
db.Entry(issue).State = System.Data.EntityState.Modified;
db.SaveChanges();
// delete - delete current relationships
if (issue.Creators != null)
{
issue.Creators = new List<Creator>();
db.Entry(issue).State = System.Data.EntityState.Modified;
db.SaveChanges();
}
// insert - get creators for many-to-many realtionship
issue.Creators = db.Creators.Where(x => CreatorIds.Contains(x.CreatorId)).ToList();
db.Entry(issue).State = System.Data.EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
IssueEditModel issueEdit = new IssueEditModel{
Creators = db.Creators.ToList(),
Issue = issue,
};
return View(issueEdit);
}
I can insert Issues and I can insert new issue.Creators without a problem. But, when I am trying to delete the current issue.Creators so I can then insert the new ones, issue.Creators is ALWAYS null so it will never update to an empty list. I don't understand this. issue.Creators has records in it because when the code proceeds to insert the new creators, I get an error like this:
Violation of PRIMARY KEY constraint 'PK__CreatorI__13D353AB03317E3D'. Cannot insert duplicate key in object 'dbo.CreatorIssues'.
The statement has been terminated.
Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.
Exception Details: System.Data.SqlClient.SqlException: Violation of PRIMARY KEY constraint 'PK__CreatorI__13D353AB03317E3D'. Cannot insert duplicate key in object 'dbo.CreatorIssues'.
The statement has been terminated.
Source Error:
Line 59: issue.Creators = db.Creators.Where(x => CreatorIds.Contains(x.CreatorId)).ToList();
Line 60: db.Entry(issue).State = System.Data.EntityState.Modified;
Line 61: db.SaveChanges();
Line 62:
Line 63: return RedirectToAction("Index");
How do I get issue.Creators to accurately show the current relationships so I can delete them?
Update: Working Controller
[HttpPost]
public ActionResult EditIssue ( Issue issue, int[] CreatorIds )
{
if(CreatorIds == null)
ModelState.AddModelError("CreatorIds", "Please specify at least one creator");
if(ModelState.IsValid)
{
// insert or update the issue record (no relationship)
if (issue.IssueId == 0)
db.Issues.Add(issue);
else
db.Entry(issue).State = System.Data.EntityState.Modified;
db.SaveChanges();
// insert and update many to many relationship
issue = db.Issues.Include("Creators").Where(x => x.IssueId == issue.IssueId).Single();
issue.Creators = db.Creators.Where(x => CreatorIds.Contains(x.CreatorId)).ToList();
db.Entry(issue).State = System.Data.EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
IssueEditModel issueEdit = new IssueEditModel{
Creators = db.Creators.ToList(),
Issue = issue,
};
return View(issueEdit);
}
The model binder isn't loading them up for you - your issues object coming from the view won't magically contain them unless you setup everything up properly for binding.
Without seeing your view one can't say why, but suffice to say you'll have to load them up, then delete them. You can load a new issues object and then do TryUpdateModel(issues) to get the form values updated into that model. Then delete each issues.Creators (if thats the intended action)
if(ModelState.IsValid)
{
var issueFromDb = db.Issues.Where(x => criteria here);
bool updateSuccessful = TryUpdateModel(issueFromDb);
foreach(var creator in issueFromDb.Creators)
{
//delete creator if thats what you want
}
However if you just want all of your creators to come back from the page without loading, check out binding to an enumerable. there are many posts out there on this, heres one: ASP.NET MVC3 Model Binding using IEnumerable (Cannot infer type from)
If the relationship is there, the Creators should automatically load just by loading the Issue. You don't need to load only the creators. Load your full model to be sure its working as I did in the edit above. Your code 'should' work ok but its possible you need to .Include("Creators") try this:
var testIssue = from o in db.Issues.Include("Creators")
where o.IssueId == issue.IssueId
select o;
foreach(var creator in testIssue.Creators)
{
//check creator
}
this will let you know if "Creators" is loading properly.
You are missing the key tag in the issue class (IssueId).
Once you have the duplicates you may need to go into the database and delete the rows manually
Related
Here is the documentation I've looked at and may be helpful: Sample SQLite OneToMany Unit Test and General Read and Write Documentation in Readme
My use-case is that I've already inserted an Item and I am now editing an Item. So I will need to basically update the Item record and insert n ItemPhoto records. Basically, I'mtalking about the case of SaveItem(..) where Item.Id != 0.
It seems that when I step through the code to write to the database I am seeing all of the keys being assigned to the objects in memory appropriately. However, later when I go to read an Item by calling GetWithChildren(..) in every case except one the ItemPhotos property has a Count of 0. The only time that ItemPhotos actually gets populated is the case when the ItemPhotoId is 0. My best guess is that somehow the ItemPhotoId is not being set before GetWithChildren(..) is run and then it only works when the default in-memory value of 0 actually matches the database's ItemPhotoId for the given Item.
Here is my code showing the models and the read and write code:
public class Item
{
[PrimaryKey, AutoIncrement]
public int Id { get; set; }
public string Text { get; set; }
public string Description { get; set; }
[OneToMany(CascadeOperations = CascadeOperation.All)]
public List<ItemPhoto> ItemPhotos { get; set; }
}
public class ItemPhoto
{
[PrimaryKey, AutoIncrement]
public int ItemPhotoId { get; set; }
[ForeignKey(typeof(Item))]
public int ItemId { get; set; }
public string FileLocation { get; set; }
[ManyToOne] // Many to one relationship with Item
public Item Item { get; set; }
}
class SqlLiteDataStore
{
static SQLiteConnection Database;
...
public Item GetItem(int id)
{
return Database.GetWithChildren<Item>(id, true);
}
public Item SaveItem(Item item)
{
// Simpler Attempt #1
// Database.InsertOrReplaceWithChildren(item);
// return item;
// Manual Attempt #2
if (item.Id != 0)
{
foreach (var photo in item.ItemPhotos)
{
if (photo.ItemPhotoId == 0)
Database.Insert(photo);
}
Database.UpdateWithChildren(item);
return item;
}
else
{
Database.InsertWithChildren(item, true);
return item;
}
}
...
}
Well I fixed it by dropping the existing tables and recreating them and beforehand just renaming the ItemPhoto's ItemPhotoId property to Id. Maybe the library assumes the primary key name of Id or maybe I changed something else along the way in terms of the models that just needed the tables to be recreated instead of migrated? Also, the simpler attempt at saving with just the Database.InsertOrReplaceWithChildren(item); seems to work just fine so I'm going with that for what it's worth.
sorry for my english, hope you're doing okay , i'm stuck in a list situation using MVC, i have a class named student
public class student
{
[Key]
public int student_Id { get; set; }
public DateTime startdate { get; set; }
public DateTime enddate { get; set; }
public statut studentstatut { get; set; }
}
public enum statut
{
Garden,
Elementary,
College,
}
i want to edit the class student , i can edit the Id , both dates , but when i want to edit the enum statut it won't , it stays in Garden and when i check my database in studentstatut it shows 0, i don't know how to make them dynamic so that i can see the studentstatut in the database and edit the statut (for exemple edited from garden to elementary ) , can anyone help please ?
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Alimentation(Student Student)
{
if (ModelState.IsValid)
{
db.Orders.Add(Student);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(Student);
}
public ActionResult Edit(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Student student= db.Students.Find(id);
if (order == null)
{
return HttpNotFound();
}
return View(student);
}
////POST: /Edit/5
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit([Bind(Include = "Student_id,startdate,enddate,AtmAmount,Statut")] Student student)
{
if (ModelState.IsValid)
{
db.Entry(student).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("index");
}
return View(student);
}
First, I would verify that you are getting the correct data back from your HttpPost Edit method. If you're able to edit and save the other fields It's possible that your client-side control is not correctly setting the correct field for your student object. Put a breakpoing in that method to verify that the studentstatut field is correctly being set.
If it is, and it is somehow not saving to your database context, you can try manually setting the field rather than using EntityState.Modified. You can do that with something along the lines of:
db.Entry(student).studentstatut = student.stdentstatut;
Though usually EntityState.Modified should do the trick.
I have created an application for several stores to fill in a questionnaire, the application has several tables within the database. Two of them are the ones I am focusing on which are Audit and Storequestions. The Audit table contains the data for the stores for example: storename, and its primary key is “AuditId”. The storesquestion table contains all the questionnaire table and AuditId as a foreign key form the Audit table.
I am wanting to create a Read Only tick box as an indication in order to find out which stores has completed the questionnaire as the only way I know which has completed the questionnaire is by looking in the database. My approach to re4solve this matter was to create a new column called read only within the Audit table with a Boolean datatype so when a store has completed the questionnaire it will set the read only row to 1(true).
Could do with a bit of Help as I don’t seem to go forward at the moment
Thanks in advance
public ActionResult Create()
{
StoreQuestions sq = new StoreQuestions();
sq.AuditId = (int)System.Web.HttpContext.Current.Session["AuditId"];
return View(sq);
}
//
// POST: /StoreQuestions/Create
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(StoreQuestions storequestions)
{
if (ModelState.IsValid)
{
db.StoreQuestions.Add(storequestions);
db.SaveChanges();
return RedirectToAction("Details", "Audit", new { id = storequestions.AuditId });
}
return View(storequestions);
}
ViewModel
public class MainModel
{
public StoreAudit StoreAudit { get; set; }
public StoreQuestions StoreQuestion { get; set; }
public List<StoreAudit> StoreAuditList { get; set; }
public List<StoreQuestions> StoreQuestionsList { get; set; }
public List<User> User { get; set; }
public List<string> StoreWindow { get; set; }
}
}
Okay I think I understand what you're trying to do. In order to update the readonly property to true you have to find the audit record and then change the value of that record's readonly property.
Like so:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(StoreQuestions storequestions)
{
if (ModelState.IsValid)
{
Audit findingAudit = db.AuditTable.Find(storequestions.AuditId);
// db being your connectionstring property
findingAudit.readonly = true;
db.StoreQuestions.Add(storequestions);
db.SaveChanges();
return RedirectToAction("Details", "Audit", new { id = storequestions.AuditId });
}
return View(storequestions);
}
I hope this helps!
I have this field called Mobile_Number in my class Friends
[Remote("CheckMobileDuplicate", "Friends", ErrorMessage = "This mobile number is already in use", AdditionalFields = "Friend_Id")]
public string Mobile_Number { get; set; }
Now there are 2 views in which post back occurs for the model class Friends, Create view and an edit view.
My CheckMobileDuplicate function is as follows
public JsonResult CheckMobileDuplicate(string Mobile_Number,int Friend_Id)
{
if (db.Friends.Any(x => (x.Mobile_Number == Mobile_Number) && (x.Friend_Id != Friend_Id))
return Json(false, JsonRequestBehavior.AllowGet);
else
return Json(true, JsonRequestBehavior.AllowGet);
}
Now the condition x.Friend_Id != Friend_Id
Checks if the Friend_Id already exists so when we are checking in edit view it does not compare the mobile_number to the mobile_number of the same friend in the database.
But in create the Friend_Id is undefined as the row in database table is not yet created and hence this is not working for create.
So how do I check in the function whether it is called from create view or edit view because making Friend_Id nullable is not an option ?
You should be using a view model for this. The RemoteAttribute is a view specific attribute and does not belong in a data model.
public class FriendsVM
{
public int? Friend_Id { get; set; }
[Remote("CheckMobileDuplicate", "Friends", ErrorMessage = "...", AdditionalFields = "Friend_Id")]
public string Mobile_Number { get; set; }
.... // other properties of Friend
}
and then in the view, add a hidden input for Friend_Id
#Html.HiddenFor(m => m.Friend_Id)
and modify the controller method to
public JsonResult CheckMobileDuplicate(string Mobile_Number, int? Friend_Id)
{
bool isUnique = IsUniqueMobile(Mobile_Number, Friend_Id);
return Json(isUnique, JsonRequestBehavior);
}
private bool IsUniqueMobile(string number, int? ID)
{
if (ID.hasValue) // its an existing Friend
{
return !db.Friends.Any(x => x.Mobile_Number == number && x.Friend_Id != ID.Value);
}
else // its a new Friend
{
return !db.Friends.Any(x => x.Mobile_Number == number);
}
}
Note that I have refactored the database access code into a private method, so that method can also be called in the POST method
I have a regular Integer (Not nullable) in my model:
[Required]
[Range(0, Int32.MaxValue - 1)]
public int PersonId
{
get;
set;
}
In my WebApi action, I accept an object that has that propery.
public IHttpActionResult Create([FromBody] Person person)
{
if (!ModelState.IsValid)
{
return BadRequest("Some error message.");
}
//Do some stuff with person...
}
Now, altough there is a Required attribute on PersonId, when a person is posted to this action, the ModelState.IsValid property is true.
I guess this is because Person is created with default value, which is 0, I want to throw an error if there is no PersonId field in the incoming JSON / query string request.
I can set PersonId to be Nullable, but that doesn't make sense.
Is there any easy way to validate the field exists and the integer is larger than 0 ? (without custom validators for that simple requirement)
Setting the [Required] attribute doesn't do anything on an int, as far as I know. All [Required] does is make sure the value is not null.
You can set [Range(1, Int32.MaxValue)] to make sure that a correct value is added.
If you don't already do this, it might be a good idea to make a different model for your view and make the data annotations on this model. I use view models to make sure I don't pollute my "real" models with stuff that is not relevant to the whole domain. This way your PersonId can be nullable in your view model only, where it makes sense.
BindRequiredAttribute can be used to
Quoting from this nice blog post about [Required] and [BindRequired]
It works the same way as RequiredAttribute, except it mandates that
the value comes from the request – so it not only rejects null values,
but also default (or “unbound”) values.
So this would reject unbound integer values:
[BindRequired]
[Range(0, Int32.MaxValue - 1)]
public int PersonId
{
get;
set;
}
I tend to use int? (nullable int) in this case and then mark those as required. I then use myInt.Value throughout the code and assume it's safe to use because it wouldn't have passed validation otherwise.
and like #andreas said, I do make sure to use "view models" in times like this so I'm not polluting my view model as a business or data layer model.
Actually for missing not nullable integer parameters model validation doesn't work. There is JSON parsing exception which is thrown by Newtonsoft.Json.
You can have a following workaround to parse and include exceptions in model validations.
Create the custom validation attribute as following and register in WebApiConfig.cs.
public class ValidateModelAttribute : ActionFilterAttribute {
public override void OnActionExecuting(HttpActionContext actionContext) {
// Check if model state is valid
if (actionContext.ModelState.IsValid == false) {
// Return model validations object
actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.BadRequest,
new ValidationResultModel(100001, actionContext.ModelState));
}
}
public class ValidationError {
public string Field { get; }
public string Message { get; }
public ValidationError(string field, string message) {
Field = field != string.Empty ? field : null;
Message = message;
}
}
public class ValidationResultModel {
public int Code { get; set; }
public string Message { get; }
public IDictionary<string, IEnumerable<string>> ModelState { get; private set; }
public ValidationResultModel(int messageCode, ModelStateDictionary modelState) {
Code = messageCode;
Message = "Validation Failed";
ModelState = new Dictionary<string, IEnumerable<string>>();
foreach (var keyModelStatePair in modelState) {
var key = string.Empty;
key = keyModelStatePair.Key;
var errors = keyModelStatePair.Value.Errors;
var errorsToAdd = new List<string>();
if (errors != null && errors.Count > 0) {
foreach (var error in errors) {
string errorMessageToAdd = error.ErrorMessage;
if (string.IsNullOrEmpty(error.ErrorMessage)) {
if (key == "model") {
Match match = Regex.Match(error.Exception.Message, #"'([^']*)");
if (match.Success)
key = key + "." + match.Groups[1].Value;
errorMessageToAdd = error.Exception.Message;
} else {
errorMessageToAdd = error.Exception.Message;
}
}
errorsToAdd.Add(errorMessageToAdd);
}
ModelState.Add(key, errorsToAdd);
}
}
}
}
}
//Register in WebApiConfig.cs
// Model validation
config.Filters.Add(new ValidateModelAttribute());