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
Related
I have the following Models:
public interface Item
{
public int Id { get; set; }
}
public class ComponentOwner : Item
{
public int Id { get; set; }
public Component Component { get; set; }
public AppUser? User { get; set; }
public DateTime ModifiedDate { get; set; }
public AppUser? UpdatedBy { get; set; }
}
public class AppUser : IdentityUser
{
public string FirstName { get; set; } = "";
public string LastName { get; set; } = "";
}
and the following async Task that saves the item to the database:
private async Task<Item> SaveItem(Item item)
{
Item updatedItem = null;
using var context = _dbContextFactory.CreateDbContext();
try
{
if (item.Id == 0)
{
context.Add(item);
await context.SaveChangesAsync();
}
When I Save a new ComponentOwner, context.Add(Item) adds the item, but also tries to add a new 'AppUser' at the same time. This causes issues because the AppUser is already created.
Is there a way that I can specify to add the ComponentOwner but not the AppUser?
as soon as 'Context.Add(item)' is hit, it wants to add an AppUser as well as the Component. I only want it to add the ComponentOwner however..
EF Core relies on tracking to determine what to do with entities. In this case it seems that item.User is not tracked, so EF tries to add it. There are multiple possible solution to this. To name a few:
If you are sure that user exists, you can just attach the entity:
if(item.User is not null)
context.Users.Attach(item.User); // or just context.Attach(item.User);
Fetch user from database and assign it to the root entity:
if (item.User is not null)
{
var user = context.Users.FirstOrDefault(u => u.Id == item.User.Id); // TODO: handle null
item.User = user;
}
Use Find:
Finds an entity with the given primary key values. If an entity with the given primary key values is being tracked by the context, then it is returned immediately without making a request to the database. Otherwise, a query is made to the database for an entity with the given primary key values and this entity, if found, is attached to the context and returned. If no entity is found, then null is returned.
if (item.User is not null)
{
var user = context.Users.Find(item.User.Id); // TODO: handle null
item.User = user;
}
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.
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'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 created an ASP.NET MVC5 sample project. I created my entities and from that, scaffolded the controllers for CRUD operations. I can only edit the POD members with the scaffolded code. I want to be able to add/remove related entities.
With my current code, when I click save there is no error but no related entities are modified (POD data is modified though). For example, if I wanted to remove all players from the account, they aren't removed. What am I doing wrong?
How can I remove/add related entities and push those changes to the database?
Here is the form:
Here is the action to update the entity:
public async Task<ActionResult> Edit([Bind(Include = "Account,Account.AccountModelId,Account.Name,Account.CreatedDate,SelectedPlayers")] AccountViewModel_Form vm){
if (ModelState.IsValid){
if (vm.SelectedPlayers != null){
vm.Account.PlayerModels = db.PlayerModels.Where(p => p.AccountModel.AccountModelId == vm.Account.AccountModelId).ToList();
foreach (var player in vm.Account.PlayerModels){
player.AccountModel = null;
db.Entry(player).State = EntityState.Modified;
}
vm.Account.PlayerModels.Clear();
foreach (var player_id in vm.SelectedPlayers){
var player = db.PlayerModels.Where(p => p.PlayerModelId == player_id).First();
vm.Account.PlayerModels.Add(player);
db.Entry(player).State = EntityState.Modified;
}
}
db.Entry(vm.Account).State = EntityState.Modified;
await db.SaveChangesAsync();
return RedirectToAction("Index");
}
return View(vm);
}
Here are the models:
public class AccountViewModel_Form{
public AccountModel Account { get; set; }
public HashSet<Int32> SelectedPlayers { get; set; }
public virtual List<PlayerModel> PlayersList { get; set; }
}
public class AccountModel{
public AccountModel(){
PlayerModels = new HashSet<PlayerModel>();
}
public Int32 AccountModelId { get; set; }
public string Name { get; set; }
public DateTime CreatedDate { get; set; }
public virtual ICollection<PlayerModel> PlayerModels { get; set; }
}
public class PlayerModel{
public Int32 PlayerModelId { get; set; }
public float Gold { get; set; }
public string Name { get; set; }
public virtual AccountModel AccountModel { get; set; }
}
I'm basically lost. I can't find any examples in how to update related data. Could someone point me in the right direction?
I come from Symfony (PHP Framework) background. I thought it would be easier but I have been having problems.
Basically I was missing the Attach function and that I had to force the load on the collection to make it work.
I found how to attach a non-attached entity here: Model binding in the controller when form is posted - navigation properties are not loaded automatically
When you post the data, the entity is not attached to the context, and when you try to save changes to a complex entity, the context makes a mess.
The code is a little different because I was trying to make it work at home. But it is essentially the same models.
public ActionResult Edit(AccountEditViewModel vm)
{
if (ModelState.IsValid)
{
//I was missing these 2 important lines...
db.Accounts.Attach(vm.Account);
db.Entry(vm.Account).Collection(a => a.Players).Load();
if (vm.SelectedPlayers != null)
{
foreach (var player in vm.Account.Players.ToList())
{
if (vm.SelectedPlayers.Contains(player.Id) == false)
{
player.Account = null;
vm.Account.Players.Remove(player);
db.Entry(player).State = EntityState.Modified;
vm.SelectedPlayers.Remove(player.Id);
}
}
foreach (var player_id in vm.SelectedPlayers)
{
var player = db.Players.Where(p => p.Id == player_id).First();
player.Account = vm.Account;
vm.Account.Players.Add(player);
db.Entry(player).State = EntityState.Modified;
}
}else
{
vm.Account.Players.Clear();
}
db.Entry(vm.Account).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(vm);
}