Unable to save many-to-many relationship - entity-framework

I have the following tables/views
Users (View)
-> UserId
-> Name
Roles (Table)
-> RoleId
-> Name
UserRoles (Table)
-> UserId
-> RoleId
and the classes
public class Role{
public int RoleId{get;set}
public string Name{get;set}
}
public class User{
public int UserId{get;set}
public string Name{get;set}
public ICollection<Role> Roles{get;set}
}
and the save method
using (var helper = new DbContext())
{
helper.Users.Attach(user);
helper.SaveChanges();
}
As you can see above, the Users is a view and UserRoles is a mapping table. I am able to retrieve User entities along with the mapped Roles. But while saving it is not throwing any exceptions nor is it saving. I tried checking the db using profiler and it is not even hitting the db.
Since Users is a view I don't want to save the User entity but only the changes made in the Roles collection.

This cannot save anything. Attach puts the whole object graph into the context, but in state Unchanged. If all objects in the context are unchanged SaveChanges won't issue any command to the DB (because for EF nothing has changed).
If you want to make changes which EF recognizes as such you must first attach the object which represents the state in the Db and then make your changes, something like:
using (var helper = new DbContext())
{
helper.Users.Attach(user);
helper.Roles.Attach(myNewRole);
user.Name = myNewName;
user.Roles.Add(myNewRole);
// etc.
helper.SaveChanges();
}
Alternatively you can mark the user as modified:
helper.Entry(user).State = EntityState.Modified;
But I believe this only affects scalar properties of the entity and it doesn't solve the problem to add a new role to the user.

Related

EF Core - many-to-many: how to add middle entity when one entity does not yet exist?

I am curios if I can make this easier/better.
The user is able to create new tickets and with each ticket can be several files associated, one file can be related to multiple tickets.
Now, when the user creates a ticket, he can already add files. Meaning I have no Id for the ticket and thus no way to build a relation. How should I solve this?
public class FilesPerTicket
{
public int TickedId;
public Ticket Ticket;
public int FileId;
public File File;
}
public class File
{
public ICollection<FilesPerTicket> FilesperTicket;
}
public class Ticket
{
public ICollection<FilesPerTicket> FilesperTicket;
}
Now the user creates the ticket and adds several files to it.
public IActionResult Create(MyModel model)
{
// .....
var filesPerTicket = model.Files.Select(x => new FilesPerTicket() { FileId = x.Value }).ToList();
var newTicket = new Ticket() { //...... };
newTicket.FilesPerTicket = filesPerTicket;
// ....
context.Add(newTicket);
context.SaveChanges();
}
This doesn't work because we haven't provided a Ticketid and therefore every TicketId in FilesPerTicket is 0.
I know that I just can save the ticket and afterwards it will have the primary key in it. Which I then can use to fill the FilesPerTicket and save that one.
But then I would have those in separate transactions (because SaveChanges was called) and use another try/catch.
Is there an way to tell EF Core to fill this TicketId automatically when this entity gets saved at the same time as a ticket? Or some other way to save that entity?

Queue<T> type with Entity Framework Cast error

I have an entity A which holds a Queue<XYZ> B property. I want to persist it in a MySQL database. When migrating, the table is created correctly - each entity A has a table depicting Queue<XYZ> B. However, when I want to query the data from the database using:
A entityA = await _context.A.Include(entityA => entityA.B).FirstOrDefaultAsync((...));
Then I get such an error:
System.InvalidCastException: Unable to cast object of type 'System.Collections.Generic.Queue'1[API.Models.XYZ]' to type
'System.Collections.Generic.ICollection'1[API.Models.XYZ]'.
When I change the Queue<XYZ> B to List<XYZ> B then it works fine.
Is it impossible to automatically persist a queue to a database with Entity Framework?
Or do I have to do some extra work?
I have an entity A which holds a Queue B property. I want to
persist it in a MySQL database. When migrating, the table is created
correctly - each entity A has a table depicting Queue B. However,
when I want to query the data from the database using:
A entityA = await _context.A.Include(entityA => entityA.B).FirstOrDefaultAsync((...));
According to your description and the query statement, I have created a DeparmentViewModel with the Queue property, and reproduced the error when query the database using EF core.
public class DepartmentViewModel
{
[Key]
public int DepId { get; set; }
public string DepName { get; set; }
public Queue<EmployeeViewModel> EmployeeViewModels { get; set; }
}
To solve this issue, it seems that we could re-generate a Queue object based on the query result, like this:
DepartmentViewModel queryresult = _context.DepartmentViewModels
.Include(c=>c.EmployeeViewModels)
.Where(c => c.DepId==2) //
.Select(c=> new DepartmentViewModel()
{
DepId = c.DepId,
DepName = c.DepName,
EmployeeViewModels = new Queue<EmployeeViewModel>(c.EmployeeViewModels.ToList())
}).FirstOrDefault();
Besides, I also found that if using the Queue, when insert new data, it might cause the following error:
"System.InvalidOperationException: 'The type of navigation property 'EmployeeViewModels' on the entity type 'DepartmentViewModel' is 'Queue' which does not implement ICollection. Collection navigation properties must implement ICollection<> of the target type.'"
So, in my opinion, when you create Navigation Properties, try to use List<T> or ICollection<T>, instead of using Queue<T>.

Entity Framework 6 - How do you add an entity with foreign key IdentityRole

I have a code first model with a mapping table so that I can map MenuItem to an IdentityRole, enabling the production of a menu based on the logged in users role assignment.
public class MenuItem
{
public int Id { get; set; }
public string Text { get; set; }
}
public class MenuRoleMap
{
[Key]
public int Id { get; set; }
public virtual MenuItem MenuItem { get; set; }
public virtual IdentityRole Role { get; set; }
}
The IdentityRole and rest of Identity is auto wired via IdentityDbContext which I've inherited through my ApplicationDbContext like this, then the context should be consistent.
public class ApplicationDbContext : IdentityDbContext<User>
All of the tables look right, they have the expected columns and foreign keys, here is the MenuRoleMap table
I have an valid existing instance of MenuItem and IdentityRole which I use to try and add a new entity item to this table
foreach (IdentityRole role in selectedRoles)
{
MenuRoleMap mrm = new MenuRoleMap();
mrm.MenuItem = menuItem;
mrm.Role = role;
db.MenuRoleMaps.Add(mrm);
}
db.SaveChanges(); /// <<<=== HERE ERROR BECAUSE THE role IS ALREADY IN DB
Which throws this error
A first chance exception of type 'System.Data.Entity.Validation.DbEntityValidationException' occurred in EntityFramework.dll
Role: Role SystemsAdministrator already exists.
Which of course, it does exist, I know that, it's already in the database. Surely the EF should not be trying to add a new entity item for the foreign key entity if it already exists?
It doesn't do it for the MenuItem, only the IdentityRole.
I thought the problem was proxy creation since the IdentityRole was a proxy object, so I turned that off
this.Configuration.ProxyCreationEnabled = false;
but I still get the same error.
My question is, how do you add an entity where the foreign key is an IdentityRole?
Thank you stackers.
ANSWER TO MY OWN QUESTION
After assistance from those below I discovered while investigating the various solutions that the problem wasn't one of context per se but the validity of an object. The object looked right, what I hadn't realised is that it wasn't the object from the context, it was a facsimile. By trying to add this facsimile to the model, the context quite rightly says it already exists, you can't add it again. By trying to override the state of the item I created a different kind of error.
The resolution was simply to reload the object from the context and then add that to the parent item like so
foreach (IdentityRole role in selectedRoles)
{
// Here I'm getting the role from the context using the ID I have from the facsimile
IdentityRole roleToUse = db.Roles.Where(x => x.Id == role.Id).FirstOrDefault();
// carry on as normal
MenuRoleMap mrm = new MenuRoleMap();
mrm.MenuItem = menuItem;
mrm.Role = roleToUse; // note I'm using the retrieved 'roleToUse'
db.MenuRoleMaps.Add(mrm);
}
db.SaveChanges();
Hey presto it all works.
It looks like you've retrieved the entity from another context and then assigned it to an entity which is then added to a different context. It would then try to insert the Role entity too.
Are you returning the Role from another method where the lifetime of the context is scoped to that method?
You may find the following link useful in regards to updating the state of objects:
Entity states and SaveChanges
The problem is that when you use db.Set<MyEntity>.Add you will mark all entities that are attached to the entity that is added as being added too. You have to explicitly mark them as being unchanged:
foreach (IdentityRole role in selectedRoles)
{
MenuRoleMap mrm = new MenuRoleMap();
mrm.MenuItem = menuItem;
mrm.Role = role;
db.MenuRoleMaps.Add(mrm);
db.Entry(role).State=EntityState.Unchanged;
}
db.SaveChanges();
My answer is you can't or at least should not.
Authentication (Roles), and Business (Menu) are different concerns of the application.
For me you have to bring in the ApplicationDb, the part of IdentityDb that you need and organize the synchronisation.
To illustrate my saying: Imaging you use Google or LiveID as authentification provider: can you imagine navigation properties from you ApplicationDd to Google or Microsoft Dbs ?
Clearly not.
So create a AppRole replicating the Role of the authentication database and use this table from your application database to build your menus.
In pseudo code this looks like:
List<Int32> l = IdentityContext.GetRolesForUser(currentUserId);
foreach (AppRole role in AppContext.Roles.Where(r => l.Contains(r.Id)))
{
MenuRoleMap mrm = new MenuRoleMap();
mrm.MenuItem = menuItem;
mrm.Role = role;
appContext.MenuRoleMaps.Add(mrm);
}
appContext.SaveChanges();
Another solution would be to use the same context for Application and Identity.
Inheritance of context seems fine, but I never tested it.

Why would this EF scenario NOT lazy load a child entity?

I have a simple controller method (via ajax) that inserts a new entity into the db and then queries the database for all of the same type of records and returns a list of those records(json).
What I can't figure out is that when I insert the first record and query for my list of records, any child entities are not lazy loaded. However, when I add a second or any number of subsequent records my list includes all child entities lazy loaded as expected. Here is some code:
public class Person
{
public int Id {get;set;}
public string Name {get;set;}
public int StateId {get;set;}
public virtual State State {get;set;}
}
public class PersonController : Controller
{
public ActionResult CreatePerson(PersonModel model)
{
var person = model.ToPersonEntity();
_personService.InsertPerson(person);
var people = _personService.GetAllPeople();//on first person inserted this list does NOT have State loaded
var personAddresssList = people.Select(x => x.ToPersonAddressFormat());
return Json(personAddressList, JsonRequestBehavior.AllowGet);
}
}
There isn't really any more relevant code as this is a pretty simple operation. I fixed the problem by using .Include(x => x.State) in my linq query but I've never had to do this as long as my properties were marked 'Virtual'.
The only thing I can think of is that EF still has my original person as a tracked entity and that when I pull up the list of persons and the only person is the one I just inserted, it uses the cached entity which would not have any child properties attached to it yet. If this is true then when I load a list of more than one person, some funky black magic in EF says "I see 2 items in the list I won't use the cached person I just inserted".
Any ideas?

Attaching Entities when using ObjectContext having lifetime of Http Request

I'm using .NET 3.5 SP1 in ASP.NET MVC application.
While using ObjectContext with Http Request lifetime, and trying to attach an entity ALREADY present in context, we get error:
"An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key."
For Example, the code :
Category newCategory = new Category {CategoryId = CategoryIdSelected};
ctx.AttachTo("CategorySet", newCategory);
will give error if 'Category' with CategoryId = CategoryIdSelected exists in ObjectContext.
Modified code to check for existing entity:
Category newCategory = new Category {CategoryId = CategoryIdSelected};
ObjectStateEntry stateEntry = null;
if( ctx.ObjectStateManager.TryGetObjectStateEntry(newCategory, out stateEntry)){
//EntityObject already attached in context, get it
newCategory = (EntityObject)stateEntry.Entity;
}else{
ctx.AttachTo("CategorySet", newCategory);
}
The modified code is still giving same error:
"[System.InvalidOperationException] = {"An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key."
Please advise ?
Thank You
QUESTION ADDENDUM:
More problems attaching Entities when using ObjectContext having lifetime of Http Request.
For Example, if we have 'AppUser','Category' and Department entities.
public class AppUser : System.Data.Objects.DataClasses.EntityObject{
public int Uid {get; set;}
public string UserName {get; set;}
public string Password {get; set;}
public Department Dept {get; set;}
public Category catg {get; set;}
...........
}
AppUser has relationship with Department and Category Entities.
Now when trying to attach 'user':
user = new AppUser{Uid=1,catg = new Category {categoryId=10}, Dept = new Department{departmentId=101}, ...}
var key = ctx.CreateEntityKey("AppUserSet", user);
if (ctx.ObjectStateManager.TryGetObjectStateEntry(key, out stateEntry)) {
will work ONLY if, in context :
there is NO Category with categoryId=10, and
there is NO Department with departmentId=101
One option, is to ensure context does not have attached entities by always retrieving using NOMERGE NoTracking option. BUT I found following problems with MergeOption.NoTracking:
Second call would still result in db hit
You don't get EntityKeys on EntityRefs. So EntityKey of XXXReference is null,which means NO FK Stub. Please see.
How to get EntityKey of Reference w/o loading both ends (both entities)?
Even though Entity are Detached, they have a reference to the DataContext (via entity._realtionships._context). Please see.
Please advise.
Thank You.
Your code is using two different contexts. You check one, and then attach to the other:
if( csContext. //...){
///
}else{
ctx. // ...
}
The entity appears to be in ctx, but not in csContext. My advice is to use only one context at a time whenever possible; it's much less confusing.
Update
OK, you've changed the code in your question.
My guess is that your stub object doesn't have an EntityKey, so TryGetObjectStateEntry is returning false. Try:
if( ctx.ObjectStateManager.TryGetObjectStateEntry(
new EntityKey("MyEntities.CategorySet", "CategoryId", CategoryIdSelected),
out stateEntry)){
Obviously, replace "MyEntities" with the actual model name.