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
Related
I am having trouble saving children entities via Entity Framework / ASP Identity. It seems to be adding duplicates of everything that is added.
I have tried using a detached graph of the DrivingLicenceModel by TeamMember.DrivingLicence = null in the TeamMemberModel and then working with a detached graph by looking if there is new or old DrivingLicenceCategories but because DrivingLicence links back to TeamMember it causes TeamMember.DrivingLicenceId to be null as it cannot link back to TeamMember.
I have tried Manually adding the EntityState to the DrivingLicence and DrivingLicenceCategories but when I do that it complains that it cannot save two entities with the same primary key.
I assume this is because they way I am copying the entities but I after a lot of looking I am drawing a blank.
If there anyway to copy from TeamMemberRequestModel to TeamMemberModel and then save without the children trying to create clone copies of themselves?
Models
public class TeamMemberModel : IdentityUser
{
public virtual DrivingLicenceModel DrivingLicence { get; set; }
public void ShallowCopy(TeamMemberRequestModel src)
{
this.DateOfBirth = src.DateOfBirth;
if (src.DrivingLicence != null)
{
if (this.DrivingLicence == null)
{
this.DrivingLicence = new DrivingLicenceModel(src.DrivingLicence);
}
else
{
this.DrivingLicence.ShallowCopy(src.DrivingLicence);
}
}
}
public TeamMemberModel() { }
}
public class DrivingLicenceModel
{
[Key]
public int Id { get; set; }
[ForeignKey("TeamMember")]
public string TeamMemberId { get; set; }
[JsonIgnore]
public TeamMemberModel TeamMember { get; set; }
public virtual List<DrivingLicenceCategoryModel> DrivingLicenceCategories { get; set; }
public DrivingLicenceModel() { }
public DrivingLicenceModel(DrivingLicenceModel src)
{
this.ShallowCopy(src);
}
public void ShallowCopy(DrivingLicenceModel src)
{
this.Id = src.Id;
this.IsFullLicence = src.IsFullLicence;
this.IssueDate = src.IssueDate;
this.ExpiryDate = src.ExpiryDate;
this.IssuingAuthority = src.IssuingAuthority;
this.LicenceNumber = src.LicenceNumber;
this.DrivingLicenceCategories = src.DrivingLicenceCategories;
this.DrivingLicencePoints = src.DrivingLicencePoints;
}
}
public class DrivingLicenceCategoryModel
{
[Key]
public int Id { get; set; }
[ForeignKey("DrivingLicence")]
public int DrivingLicenceId { get; set; }
[JsonIgnore]
public DrivingLicenceModel DrivingLicence { get; set; }
}
public class TeamMemberRequestModel
{
public string Id { get; set; }
public virtual DrivingLicenceModel DrivingLicence { get; set; }
}
Context
public class TIERDBContext : IdentityDbContext<TeamMemberModel, RoleModel, string, IdentityUserLogin, IdentityUserRole, IdentityUserClaim>
{
public TIERDBContext() : base("SARDBConnection") { }
public DbSet<DrivingLicenceModel> DrivingLicences { get; set; }
public DbSet<DrivingLicenceCategoryModel> DrivingLicenceCategories { get; set; }
}
Controller
public async Task<IHttpActionResult> Put(string id, TeamMemberRequestModel teamMember)
{
TeamMemberModel CurrentTeamMember = await this.TIERUserManager.FindByIdAsync(id);
CurrentTeamMember.ShallowCopy(teamMember);
await this.TIERUserManager.UpdateAsync(CurrentTeamMember);
}
you have to create clone property into context class
.
In the context clases you could to use clone method that retiran the entity you send by parameters this duplicarse any entity you pass. Sorry for my english
hope you help
After far to many hours working over this. I have come to an answer. The best way to deal with this is to simply deal with it is to add or attach all entities down the tree.
The controller now attaches all children unless they have an ID of 0, therefore new and uses add instead. Then I use this very useful extension I found here http://yassershaikh.com/c-exceptby-extension-method/ to compare lists to see added and deleted entities in the list. While I don't need the added part as the entity will already be marked to an add state as I use add() it does not harm and I want to use it later with add and delete state changing.
Controller
public async Task<IHttpActionResult> Put(string id, TeamMemberRequestModel teamMember)
{
TIERDBContext IdentityContext = (TIERDBContext)this.TIERUserManager.UserStore().Context;
foreach (DrivingLicenceCategoryModel DrivingLicenceCategory in teamMember.DrivingLicence.DrivingLicenceCategories)
{
if (DrivingLicenceCategory.Id == 0)
{
IdentityContext.DrivingLicenceCategories.Add(DrivingLicenceCategory);
}
else
{
IdentityContext.DrivingLicenceCategories.Attach(DrivingLicenceCategory);
}
}
foreach (DrivingLicencePointModel DrivingLicencePoint in teamMember.DrivingLicence.DrivingLicencePoints)
{
if (DrivingLicencePoint.Id == 0)
{
IdentityContext.DrivingLicencePoints.Add(DrivingLicencePoint);
}
else
{
IdentityContext.DrivingLicencePoints.Attach(DrivingLicencePoint);
}
}
this.DetectAddedOrRemoveAndSetEntityState(CurrentTeamMember.DrivingLicence.DrivingLicenceCategories.AsQueryable(),teamMember.DrivingLicence.DrivingLicenceCategories, IdentityContext);
this.DetectAddedOrRemoveAndSetEntityState(CurrentTeamMember.DrivingLicence.DrivingLicencePoints.AsQueryable(),teamMember.DrivingLicence.DrivingLicencePoints, IdentityContext);
CurrentTeamMember.ShallowCopy(teamMember);
await this.TIERUserManager.UpdateAsync(CurrentTeamMember);
}
I then use a generic that uses ExceptBy to work out what is added and delete from the old team member model to the new team member model.
protected void DetectAddedOrRemoveAndSetEntityState<T>(IQueryable<T> old, List<T> current, TIERDBContext context) where T : class, IHasIntID
{
List<T> OldList = old.ToList();
List<T> Added = current.ExceptBy(OldList, x => x.Id).ToList();
List<T> Deleted = OldList.ExceptBy(current, x => x.Id).ToList();
Added.ForEach(x => context.Entry(x).State = EntityState.Added);
Deleted.ForEach(x => context.Entry(x).State = EntityState.Deleted);
}
It works but it is far from great. It takes two DB queries, getting the original and updating. I just cannot think of any better way to do this.
I'm a newbie to ASP.NET using Entity Framework. I have different models for People, FileType and FilePath. I want to display the image by retrieving the file path from FilPath together with data like name, age, etc. in index view. I made it happen in Detail view, but in index view page I received error as "Value can not be null", which caused by the FilePath in PeopleDB is null.
Below is my code, please help. Thanks.
/Model/PeopleDB.cs
namespace MvcDemo.Models {
public class PeopleDB
{
public PeopleDB()
{
this.FilePaths = new HashSet<FilePath>();
}
public int ID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
public string Address { get; set; }
public string Interests { get; set; }
public ICollection<FilePath> FilePaths { get; set; }
}
public class PeopleDBContext : DbContext
{
public DbSet<FilePath> FilePaths { get; set; }
public DbSet<PeopleDB> People { get; set; }
}
}
/Model/FilePath.cs
namespace Assessment_HC.Models
{
public class FilePath
{
public int FilePathId {get;set;}
[StringLength(255)]
public string FileName {get;set;}
public FileType FileType {get;set;}
public int PersonID {get;set;}
public virtual PeopleDB Person {get;set;}
}
}
Moedel/FileType.cs
namespace Assessment_HC.Models
{
public enum FileType
{
Avatar = 1, Photo
}
}
Here is the controller for index view
//Get: /People/Index
public ActionResult Index()
{
return View(db.People.ToList());
}
In db.People.ToList(), People.FilePath view is null.
In the controller, the detail view is like this, from where I can get the image showing on detail page:
// GET: /People/Details
public ActionResult Details(int id = 0)
{
PeopleDB peopledb = db.People.Find(id);
PeopleDB people = db.People.Include(i => i.FilePaths).SingleOrDefault(i => i.ID == id);
if (peopledb == null)
{
return HttpNotFound();
}
return View(peopledb);
}
Thanks for your help. Let me know if you need more code.
Based on comments, It seems the only thing you should do is changing FilePaths property of your PeopleDB to be virtual to work with Lazy Loading (which is enabled by default):
public virtual ICollection<FilePath> FilePaths { get; set; }
Lazy Loading is enabled by default, and as stated in comments you didn't change it and there is nothing about Lazy Loading in your context constructor, So it seems the problem is in your FilePaths navigation property that is not virtual.
For index action:
return View(db.People.ToList());
For details action its better to do like:
var people = db.People.Where(x => x.ID == id).FirstOrDefault();
if (people == null)
{
return HttpNotFound();
}
return View(people );
But any way, If disable lazy Loading, you should use Include to include your navigation property in result. In this situation you can load data in your index action use:
db.People.Include(x => x.FilePaths).ToList()
or
//Remember to add using System.Data.Entity;
db.People.Include("FilePaths").ToList()
And to disable Lazy Loading you can
db.Configuration.LazyLoadingEnabled = true;
Or in the constructor of your context:
this.Configuration.LazyLoadingEnabled = false;
More information:
Loading Related Entities
Lazy loading is the process whereby an entity or collection of
entities is automatically loaded from the database the first time that
a property referring to the entity/entities is accessed. When using
POCO entity types, lazy loading is achieved by creating instances of
derived proxy types and then overriding virtual properties to add the
loading hook.
I've tested the code, the only one thing that you need is enabling Eager loading using Include method:
public ActionResult Index()
{
var _db = new ApplicationDbContext();
var model = _db.People.Include("FilePaths").ToList();
return View(model);
}
In this case all related file paths will be loaded.
You can also make FilePaths as virtual:
public virtual ICollection<FilePath> FilePaths { get; set; }
And change your query this way:
var model = _db.People.ToList();
In both cases, all related file paths will be loaded.
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);
}
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();
This is my code from my controller:
MGEntities db = new MGEntities();
[HttpPost]
public ActionResult Register(RegisterModel model)
{
if (ModelState.IsValid)
{
// Attempt to register the user
MembershipCreateStatus createStatus = MembershipService.CreateUser(model.UserName, model.Password, model.Email);
if (createStatus == MembershipCreateStatus.Success)
{
FormsService.SignIn(model.UserName, false /* createPersistentCookie */);
MembershipUser myObject = Membership.GetUser();
Guid UserID = (Guid)myObject.ProviderUserKey;
MyProfile profile = new MyProfile();
profile.Address = model.Address;
profile.City = model.City;
profile.Zip = model.Zip;
profile.State = model.State;
profile.UserId = UserID;
Debug.Write(profile.State);
db.aspnet_Profiles.Add(profile);
return RedirectToAction("Index", "Home");
}
else
{
ModelState.AddModelError("", AccountValidation.ErrorCodeToString(createStatus));
}
}
This is my MyProfile Class:
namespace MatchGaming.Models
{
[Bind(Exclude = "ProfileId")]
public class MyProfile
{
[Key]
[ScaffoldColumn(false)]
public int ProfileId { get; set; }
public Guid UserId { get; set; }
[DisplayName("Address")]
public string Address { get; set; }
[DisplayName("City")]
public string City { get; set; }
[DisplayName("Zip")]
public string Zip { get; set; }
[DisplayName("State")]
public string State { get; set; }
}
}
After the linq query is executed, i check my database and nothing is added. I am using POCO for my entities. Here is my class:
namespace MatchGaming.Models
{
public class MGEntities : DbContext
{
public DbSet<MyProfile> aspnet_Profiles { get; set; }
}
}
I basically just dont understand why its not adding to the database, if theres a way I can check if the query went through correctly or not or if anyone can see the problem. Thank you!
Try
db.aspnet_Profiles.Add(profile);
db.SaveChanges();
You're misusing EF.
EF contexts are not thread safe and cannot be reused across requests.
You need to create a separate context (MGEntities) for each request, by creating it in the controller in a using statement.
You also need to call SaveChanges().