I have a class named Classroom which is something like:
public class Classroom
{
[Key]
public int ClassroomId { get; set; }
public string ClassroomTitle { get; set; }
public string AccessCode { get; set; }
public string ColorPicker { get; set; }
public LevelGroup LevelGroup { get; set; }
}
The LevelGroup class is something like:
public class LevelGroup
{
public int MLevelId { get; set; }
public int MGroupId { get; set; }
public Level Level { get; set; }
public Group Group { get; set; }
}
In my API, I am trying to retrieve the data of type Classroom like:
[HttpPost("AddClassroom")]
public async Task<JsonResult> AddClassroom([FromBody] Classroom classroom)
{
if (!ModelState.IsValid)
{
return Json(new ApiMessage
{
HasError = true,
Message = "InvalidModel",
});
}
try
{
_context.Add(classroom);
await _context.SaveChangesAsync();
return Json(new ApiMessage
{
HasError = false,
Message = "Success"
});
}
catch (Exception e)
{
return Json(new ApiMessage
{
HasError = true,
Message = e.Message
});
}
}
From the POSTMAN, I tried to hit the API at some url and in the BODY, I've passed this object:
{
"classroomTitle": "SWE",
"accessCode": "6s1x4d1",
"colorPicker": "blue",
"levelGroup": {
"mLevelId": 1,
"mGroupId": 2
}
}
But, This is not working. It says:
An exception occurred in the database while saving changes for context type 'mirror_api.Models.ApplicationDbContext'.
Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while updating the entries. See the inner exception for details.
---> Microsoft.Data.Sqlite.SqliteException (0x80004005): SQLite Error 19: 'UNIQUE constraint failed: LevelGroups.MGroupId, LevelGroups.MLevelId'.
How to solve this problem?
Based on Your comments I understand You want to save passed object but You do not want to save inner property of it as it violates some DB constraints.
What I would try is to detach those properties from EF tracking before saving so it won't mark classroom.LevelGroup as Added. You can see this example. You can also control which objects EF is tracking for changes by setting a proper state for each individual property that was added to the EF context (docs).
You also want to read this which nearly describes what You seek:
var existingBlog = new Blog { BlogId = 1, Name = "ADO.NET Blog" };
using (var context = new BloggingContext())
{
context.Blogs.Attach(existingBlog);
context.Entry(existingBlog).State = EntityState.Unchanged;
// Do some more work...
context.SaveChanges();
}
but instead of attaching the object, You want to add it and then set one of its properties as EntityState.Detached to ignore it completly (or EntityState.Unchanged to keep tracking it but tell EF that there is nothing to save here). Something more like this:
...
_context.Add(classroom);
_context.Entry(classroom.LevelGroup).State = EntityState.Detached;
await _context.SaveChangesAsync();
...
The moment You add the object, EF "gegins tracking the given entity, and any other reachable entities" and You can "Use State to set the state of only a single entity" as shown.
Related
Since adding logging functionality to my Entity Framework project, I have been unable to delete any records from the table.
Here are the data classes for the objects which are added to the database, the user to the user table and the log to the logs table:
public class User
{
public string UserName { get; set; }
[Key]
public string ApiKey { get; set; } //unique database key and API key for user
public ICollection<Log> Logs { get; set; }
public User() { }
}
public class Log
{
[Key]
public int logID { get; set; }
public string logString { get; set; }
public string logDateTime { get; set; }
public string userAPIKey { get; set; }
public Log() { }
}
Here is how logs are added to the table, as since adding logging I've been having the issue:
public void addLogToUserWithApiKey(string logMessage, string apiKey)
{
Log newLog = new Log();
newLog.logID = makeLogID();
newLog.logString = logMessage;
newLog.logDateTime = DateTime.Now.ToShortDateString() + " " + DateTime.Now.ToString("h:mm:ss tt");
newLog.userAPIKey = apiKey;
using (var context = new UserContext())
{
User logUser = checkIfUserExistsWithApiKeyandReturnUser(apiKey);
if (logUser.Logs == null)
{
logUser.Logs = new Collection<Log>();
}
logUser.Logs.Add(newLog);
context.Logs.Add(newLog);
context.SaveChanges();
}
}
And finally, this is the code to delete a record:
public void deleteUserFromDatabase(string mApiKey)
{
using (var context = new UserContext())
{
try
{
User userToDelete = checkIfUserExistsWithApiKeyandReturnUser(mApiKey);
if (userToDelete != null)
{
context.Users.Attach(userToDelete);
context.Users.Remove(userToDelete);
context.SaveChanges();
}
}
catch (Exception e) { }
}
}
There were no exceptions being called when the delete method was like that however it still wasn't working.
I changed the delete method to this:
User userToDelete = checkIfUserExistsWithApiKeyandReturnUser(mApiKey);
if (userToDelete != null)
{
if (userToDelete.Logs != null)
{
userToDelete.Logs.ToList().ForEach(log => userToDelete.Logs.Remove(log));
}
context.Users.Attach(userToDelete);
context.Users.Remove(userToDelete);
context.SaveChanges();
}
And I got this error message:
The DELETE statement conflicted with the REFERENCE constraint "FK_dbo.Logs_dbo.Users_User_ApiKey". The conflict occurred in database "SecuroteckWebApplication.Models.UserContext", table "dbo.Logs", column 'userAPIKey'. The statement has been terminated.
If you want to delete a User, you would have to delete all Log entries associated with that user first.
var apiKey = String.Empty; // The id of the user you want to delete
using (var context = new UserContext())
{
User userToDelete = checkIfUserExistsWithApiKeyandReturnUser(apiKey);
if (userToDelete != null)
{
var userLogs = context.Logs.Where(l => l.userAPIKey == apiKey);
if (userLogs.Any())
{
context.Logs.RemoveRange(userLogs);
}
context.Users.Attach(userToDelete);
context.Users.Remove(userToDelete);
context.SaveChanges();
}
}
This error is returned from SQL Server. As it says, you cannot delete the User because there are records from the dbo.Logs table which are related to the deleted user and there is a foreign key defined linking the userAPIKey column with this deleted User.
Seeing your entity code, I can't tell why the foreign key was created in the first place, if you are using Entity Framework Code First. If this is your case, probably you are falling in an Entity Framework convention rule.
Anyways, there are some ways to solve this.
If you are using EF Code First. Delete all the logs pointing to the deleted user or update them setting them to NULL, depending on how much you need to preserve the logs for a deleted user.
EDIT: As the OP is using Code First, then the relationship between Log and User is not completely defined. This is the proper entity code if a strong relationship is what is intended by the OP.
public class User
{
public string UserName { get; set; }
[Key]
public string ApiKey { get; set; } //unique database key and API key for user
[InverseProperty("User")]
public virtual ICollection<Log> Logs { get; set; }
public User() { }
}
public class Log
{
[Key]
public int logID { get; set; }
public string logString { get; set; }
public string logDateTime { get; set; }
public string userAPIKey { get; set; }
[ForeignKey("userAPIKey")
public virtual User User {get; set; }
public Log() { }
}
With the strong relationship, logs should be deleted or set to null before being able to the delete the user if cascade conventions are not configured.
I have two tables written in EF CodeFirst:
public class DayType
{
[Key]
public int DayTypeID { get; set; }
public string NameDayType { get; set; }
public virtual ICollection<SpecialDay> Specialdays { get; set; }
public DayType() { }
}
public class SpecialDay
{
[Key]
public int SpecialDayID { get; set; }
public int Year { get; set; }
public int JanuaryDay { get; set; }
public SpecialDay() { }
public int? DayTypeId { get; set; }
public virtual DayType daytype { get; set; }
}
DBContext relation one-to-many were made by FluentAPI:
modelBuilder.Entity<DayType>().HasMany(p => p.Specialdays).WithOptional(p => p.daytype);
This code throw exception. The purpose of function is to update entity. While debugging sd had all properties. sd - is object which were selected from datagrid and then changed.
internal void Update(SpecialDay sd)
{
using (SalDBContext _db = new SalDBContext())
{
var newsd = _db.SpecialDays.FirstOrDefault(p => p.SpecialDayID==sd.SpecialDayID);
newsd.JanuaryDay = sd.JanuaryDay;
....
newsd.DecemberDay = sd.DecemberDay;
newsd.DayTypeId = sd.DayTypeId;
newsd.daytype = sd.daytype;
try
{
_db.SaveChanges();
}
catch (Exception ex)
{
MessageBox.Show(ex.ToString());
}
}
At these point exception is happened _db.SaveChanges(); Exception:
{"The relationship between the two objects cannot be defined because they are attached to different ObjectContext objects."} System.Exception {System.InvalidOperationException}
Would be thankful for any help with solution for my problems. Thank you
As the exception states,
"The relationship between the two objects cannot be defined because they are attached to different ObjectContext objects."
Looking at the code you have there, you are assigning the daytype from the one passed in to the one you pulled from the database. Because you're creating a new SalDBContext every time you call into the Update method, you're assigning the daytype across ObjectContext objects (per the error message.
To get around that, you just need to eliminate that assignment in your Update method. Because you're assigning the FK ID in the property, you do not also need to assign the object.
One other note on EF, there is also a Find method which will go just by the ID instead of FirstOrDefault. It's a bit more optimized.
internal void Update(SpecialDay sd)
{
using (SalDBContext _db = new SalDBContext())
{
var newsd = _db.SpecialDays.Find(p => p.SpecialDayID==sd.SpecialDayID);
newsd.JanuaryDay = sd.JanuaryDay;
....
newsd.DecemberDay = sd.DecemberDay;
newsd.DayTypeId = sd.DayTypeId;
// newsd.daytype = sd.daytype; Must be eliminated!
try
{
_db.SaveChanges();
}
catch (Exception ex)
{
MessageBox.Show(ex.ToString());
}
}
You have loaded sd (from method head) with anonther context as the one in your method body newsd. Therefore you received an exception on calling _db.SaveChanges();. I would say in line newsd.daytype = sd.daytype;, because it's a complex data type and represents one of your entities. Remove the line and set only the ID or load the obejct sd again for referencing.
Modified version of yours:
internal void Update(SpecialDay sd)
{
using (SalDBContext _db = new SalDBContext())
{
var newsd = _db.SpecialDays.FirstOrDefault(p => p.SpecialDayID==sd.SpecialDayID);
newsd.JanuaryDay = sd.JanuaryDay;
// ...
newsd.DecemberDay = sd.DecemberDay;
// set only the id to reference the object
newsd.DayTypeId = sd.DayTypeId;
// newsd.daytype = sd.daytype;
try
{
_db.SaveChanges();
}
catch (Exception ex)
{
MessageBox.Show(ex.ToString());
}
}
}
Well I was some seconds to late. :)
I'm attempting to create an Audit Log for my MVC, Entity Framework website project. I've been able to subscribe to SaveChanges() in my DBContext (and save to my database through another DBContext but same database).
My two questions in the end are:
What does if (!entry.IsRelationship) do exactly? I have a ViewModel that calculates this as True when Saving and another as False. I would expect this to move into the rest of my method to save in the Audit Log.
How can I get the full Namespace of my Object being modified? I was using this: entry.Entity.ToString() but doesn't seem to work when Saving/Editing from a View Model (details below)
Here is a basic setup that I have thus far (Album object/controller works, but AlbumView doesn't):
Ablum class:
public class Album : BaseObject //BaseObject has a few properties, one is Oid (Guid)
{
public string Name { get; set; }
[Column(TypeName = "varchar(MAX)")]
[DataType(DataType.MultilineText)]
public string Description { get; set; }
[Display(Name="Genres")]
public virtual ICollection<AlbumsGenres> AlbumGenres { get; set; }
[Display(Name="Artists")]
public virtual ICollection<AlbumsArtists> AlbumArtists { get; set; }
}
AblumView class:
public class AlbumView
{
[ScaffoldColumn(false)]
public Guid Oid { get; set; }
[Required]
public string Name { get; set; }
[Column(TypeName = "varchar(MAX)")]
[DataType(DataType.MultilineText)]
public string Description { get; set; }
[Display(Name = "Genres")]
public virtual List<AlbumsGenres> AlbumGenres { get; set; }
[Display(Name = "Artists")]
public virtual List<AlbumsArtists> AlbumArtists { get; set; }
}
AlbumsController (Audit works with something like this):
public ActionResult Edit(Album album)
{
if (ModelState.IsValid)
{
db.Entry(album).State = EntityState.Modified;
db.SaveChanges(); //This is where SaveChanges() takes over (see below)
return RedirectToAction("Index");
}
return View(album);
}
AlbumsViewController:
public ActionResult Edit(Guid id, AlbumView albumViewModel)
{
//Omitting setup...
//Album gets updated
Album album = db.Albums.Find(id);
album.Name = albumViewModel.Name;
album.Description = albumViewModel.Description;
//Other Objects are also updated, just an example:
albumArtists = new AlbumsArtists();
albumArtists.Oid = Guid.NewGuid();
albumArtists.Album = db.Albums.Find(id);
albumArtists.Artist = db.Artists.Find(item.Artist.Oid);
//In the end it calls:
db.SaveChanges();
//Omitting other stuff...
}
On db.SaveChanges() within my DbContext:
public class ApplicationDBContext : DbContext
{
public ApplicationDBContext() : base("name=DefaultConnection") { }
public System.Data.Entity.DbSet<ContentPub.Models.Music.Album> Albums { get; set; }
//Other DBSet objects...
public DbSet Set(string name)
{
return base.Set(Type.GetType(name));
}
public override int SaveChanges()
{
ApplicationLogDBContext logDb = new ApplicationLogDBContext();
ChangeTracker.DetectChanges();
ObjectContext ctx = ((IObjectContextAdapter)this).ObjectContext;
List<ObjectStateEntry> objectStateEntryList =
ctx.ObjectStateManager.GetObjectStateEntries(EntityState.Added
| EntityState.Modified
| EntityState.Deleted)
.ToList();
foreach (ObjectStateEntry entry in objectStateEntryList)
{
Guid oid = Guid.Empty;
try
{
if (!entry.IsRelationship) //I don't understand this (first of my two questions)
{
switch (entry.State)
{
//Removed other cases
case EntityState.Modified:
{
oid = (Guid)entry.EntityKey.EntityKeyValues[0].Value;
//This is the area that I am having issues (second of the two questions)
//Below will work when I call db.SaveChanges() from the AlbumsController,
//'entry.Entity.ToString()' will get 'x.Models.Music.Albums' and begin a query
var query = this.Set(entry.Entity.ToString()).AsNoTracking().Where("Oid == #0", oid);
//The issue with the above is when I have a ViewModel, returns something like
// = System.Data.Entity.DynamicProxies.Album_AF81C390156ACC8283ECEC668AFB22C4AD621EF70F8F64641D56852D19755BF3
//If the proper Namespace is returned, the next line works and Audit continues
var query = this.Set(entry.EntitySet.ElementType.ToString()).AsNoTracking().Where("Oid == #0", oid);
//Does a bunch of AuditLog stuff if the above issue doesn't fail
break;
}
}
}
}
catch (Exception ex)
{
throw new Exception("Log Error (" + entry.Entity.ToString() + ") - " + ex.ToString());
}
}
return base.SaveChanges();
}
}
entry.Entity.ToString() will return something like:
System.Data.Entity.DynamicProxies.Album_AF81C390156ACC8283ECEC668AFB22C4AD621EF70F8F64641D56852D19755BF3
In the AlbumView I am updating Album, and a bunch of other Objects. Not sure why it isn't returning x.Models.Music.Albums, is there a work-around, can someone explain or point me to other resources that I haven't found yet?
While it isn't the most efficient solution, it still is a solution for now.
I was able to do the following inside my db.SaveChanges() method:
//When AlbumView .BaseType was able to return x.Models.Music.Album
string strNamespace = entry.Entity.GetType().BaseType.ToString();
//Needed this if I was updating just an Object (ie: Album),
//would be nice to make something more concret
if (strNamespace == "x.Models.Core.BaseObject")
strNamespace = entry.Entity.ToString();
//Continuing code
var query = this.Set(strNamespace).AsNoTracking().Where("Oid == #0", oid);
Found the answer here from another Question that I had not found before posting this question
I have this model
public class CPAppModel
{
[Key]
public string AppId { get; set; }
public string AppName { get; set; }
public string AppDescription { get; set; }
public virtual CPAppCategoryModel Category { get; set; }
public string Tags { get; set; }
}
and i use the following code to Add/Update entries
var db = new CheckpointApplicationContext();
if (int.Parse(AppModel.AppId) == 0) return;
if (!db.AppExist(AppModel))
{
db.Applications.Add(AppModel);
db.SaveChanges();
Message = "Record Added!";
}
else
{
var existingEntry = db.Applications.First(a => a.AppId == AppModel.AppId);
existingEntry.AppDescription = AppModel.AppDescription;
existingEntry.AppName = AppModel.AppName;
existingEntry.Category = AppModel.Category;
existingEntry.Tags = AppModel.Tags;
existingEntry.AppDescription = AppModel.AppDescription;
db.SaveChanges();
Message = "Record Updated!";
}
the problem is in the Category update.
The scenario is - I might be getting a new category for my entity or a different (existing Category).
I thought that when using lazy loading, it would update it "magically" all by itself - this is not the case.
I', getting a primary key error.
Is there an easier way of achieving Add/Update logic for my "Entire Entity"
Attach has not worked for me
AddObject is not an option.
I found that I had forgotten to add the categories as a dbset
once I had that - attach started working.
db.Categories.Attach(AppModel.Category);
db.Applications.Attach(AppModel);
db.SaveChanges();
Creating an AngularJS application based off of this tutorial: http://jphoward.wordpress.com/2013/01/04/end-to-end-web-app-in-under-an-hour/
Classes:
public class Todo
{
public int ID { get; set; }
public virtual Status Status { get; set; }
}
public class Status
{
public int ID { get; set; }
public string Type { get; set; }
}
Functionality is that you click a button and it changes the status. When the button is clicked, all the right things are being passed in to Visual Studio. Originally it wasn't updating at all. After some research I found some ways to force the changes, but then at db.SaveChanges() it adds a new row to Status that has the same 'Type', just an incremented ID from whatever the last one is at.
JS that calls update:
Api.Todo.update({ id: todoID }, todo, function () {
$location.path('/');
});
Which hits VS on this function:
private DataContext db = new DataContext();
// PUT api/Todo/5
HttpResponseMessage PutTodo(int id, Todo todo)
{
if (!ModelState.IsValid)
{
return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
}
if (id != todo.ID)
{
return Request.CreateResponse(HttpStatusCode.BadRequest);
}
// Found a stack overflow that mentioned that you need to check for things already being tracked
var entry = db.Entry(todo);
if (entry.State == EntityState.Detached)
{
var set = db.Set<Todo>();
Todo attachedEntity = set.Find(todo.ID); // You need to have access to key
if (attachedEntity != null)
{
// The following code does not update any changes to the foreign keys
var attachedEntry = db.Entry(attachedEntity);
attachedEntry.CurrentValues.SetValues(todo);
db.Entry(attachedEntity).State = EntityState.Modified;
// When this didn't work, I tried just changing the status on the already attached entity
//attachedEntity.Status = todo.Status;
//db.SaveChanges();
// However when it hit SaveChanges() it created a new row in the Status table.
}
else
{
//This code was never hit
entry.State = EntityState.Modified; // This should attach entity
}
}
try
{
db.SaveChanges();
}
catch (DbUpdateConcurrencyException ex)
{
return Request.CreateErrorResponse(HttpStatusCode.NotFound, ex);
}
I'm nearing the end of my capabilities and would love a little help or direction.
public class Todo
{
public int ID { get; set; }
//Foreign key
public int StatusID { get; set; } //<====Add this line
public virtual Status Status { get; set; }
}
When you post your data update the foreign key property,not the navigational property
Also ,check this:
Why Does Entity Framework Reinsert Existing Objects into My Database?
http://msdn.microsoft.com/en-us/magazine/dn166926.aspx