ef6 set only foreign key property of navigation property while saving - entity-framework

When I save entity which has navigation property which I use as foreign key:
this.HasRequired<Role>(c => c.Role).WithMany().Map(c => c.MapKey("role_id"));
I set only foreign key property of this navigation property (I get it from web page) thereby other properties of this navigation property are empty, but they have required restrictions:
this.Property(c => c.RoleName).IsRequired();
It's the reason why I get "dbentityvalidationexception" exception with error "field is required".
Is it possible to solve this problem by somehow?
Or I must get full entity for that navigation property from DB, set navigation property of entity which I save and then save my initial entity (It works now, but it doesn't look like good solution)?
Thanks in advance.
This is MS MVC action where I handle the model:
[HttpPost]
public async Task<ActionResult> AddAsync(Staff staff)
{
await staffService.InsertAsync(staff);
return RedirectToAction("Index");
}
and that part of view where I set the property:
<dt>
#Html.Label("Role")
</dt>
<dd>
#Html.DropDownListFor(_=>_.Role.Id, new SelectList(ViewBag.Roles, "Id", "RoleName"), "- Please select a Role -")
</dd>
this is the type of model "Staff"
public class Staff
{
public Staff()
{
Name = new Name();
}
public int Id { get; set; }
public Name Name { get; set; }
...
public virtual Role Role { get; set; }
...
}

I have found more or less good solution for that problem.
I have set all navigation property for which I have only foreign key property as
EntityState.Unchanged.
Now I have this method for saving only specific entity (Staff)
public virtual Staff InsertStaff(Staff entity)
{
context.Entry(entity.Role).State = EntityState.Unchanged;
context.Entry(entity.Maneger).State = EntityState.Unchanged;
SetStaffNavigationPropertiesUnchanged(entity);
return dbSet.Add(entity);
}
and this for saving full graph (in base generic class):
public virtual TEntity InsertGraph(TEntity entity)
{
return dbSet.Add(entity);
}
using "EntityState.Unchanged" (I will show in simplest way) lets me saving Staff when Role has only foreign key property filled (Role has RoleName required property)
using (ClinchContext context = new ClinchContext())
{
var role = new Role { Id = 1 };
Staff staff = context.Staffs.Add(new Staff
{
Name = new Name { Firstname = "FN", Surname = "S", Patronymic = "P" },
Email = "E",
Login = "L",
IsDeleted = false,
Role = role,
Maneger = null//nullable for now
});
context.Entry(staff.Role).State = EntityState.Unchanged;
context.SaveChanges();
}
If someone has more proper solution I would be happy to know, Thank you.

Related

.NET Core EF Primary Key Error on context.SaveChanges()

My Project model has a collection of <AppUsers>:
public class Project
{
public int ProjectID { get; set; }
[Required(ErrorMessage = " Please enter a project name")]
public string Name { get; set; }
[Required(ErrorMessage = " Please enter a project description")]
public string Description { get; set; }
public virtual ICollection<AppUser> ProjectManagers { get; set; }
}
public class AppUser : IdentityUser
{
//just a placeholder for now until I add properties later
}
In my EF repository, I get an error when I try to assign any user to more than one project as a part of the ProjectManager collection (e.g., I can assign them to Project1 but when I assign them to Project2 it throws an exception).
public void AddProjectManager(int projectID, AppUser user)
{
Project proj = context.Projects.Include(p => p.ProjectManagers).FirstOrDefault(p => p.ProjectID == projectID);
if (!proj.ProjectManagers.Any(pm => pm.Id == user.Id))
{
AppUser appUser = identityContext.AspNetUsers.FirstOrDefault(p => p.Id == user.Id);
if (appUser != null)
{
proj.ProjectManagers.Add(appUser);
//ERROR OCCURS HERE WHEN I TRY TO SAVE
context.SaveChanges();
}
}
}
I get the following error:
Violation of PRIMARY KEY constraint 'PK_AppUser'. Cannot insert duplicate key in object 'dbo.AppUser'. The duplicate key value is (xxx).
I've tried using a UserManager approach, but get the same error
Normally this would result in a multiple instances of IEntityChangeTracker (see: Why is my entity being referenced by multiple instances of IEntityChangeTracker?) however I suspect your identityContext has lazy loading proxies turned off, so you'll get the PK exception.
The issue will be that you are loading the user from one context, (identityContext) and trying to save a reference to it via another context. (context) You want to load the reference from the same context. The application context knows about AppUser, but it didn't load the instance you are associating to the project because that was loaded from a different context so it is treating it as a new AppUser.
The solution is to load the AppUser from context rather than identityContext
public void AddProjectManager(int projectID, AppUser user)
{
Project proj = context.Projects.Include(p => p.ProjectManagers).FirstOrDefault(p => p.ProjectID == projectID);
if (!proj.ProjectManagers.Any(pm => pm.Id == user.Id))
{
AppUser appUser = context.AspNetUsers.Single(p => p.Id == user.Id);
proj.ProjectManagers.Add(appUser);
context.SaveChanges();
}
}
If you don't have a DbSet for the AspNetUser in the application context, you would need to add it. Normally for something like this, I wouldn't add the entire AspNetUser entity to the application context, but rather a lightweight entity with just the details I need (ID and Name for instance) and then my application entities would reference this lightweight instance (pointed at the same table) to speed up data operations for this association.

Usermanager with model inherited from applicationUser

I'm early beginner in ASP.NET, and I'm trying to build user system using Identity framework in ASP.NET-MVC project.
I want to have "Admin" user entity inherit from base entity "applicationUser" which is created by default in my models (with my minor changes):
public class ApplicationUser : IdentityUser
{
[Required]
[StringLength(50)]
[Display(Name = "Last Name")]
public string LastName { get; set; }
[Required]
[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
[Column("FirstName")]
[Display(Name = "First Name")]
public string FirstMidName { get; set; }
[Display(Name = "Full Name")]
public string FullName
{
get
{
return LastName + ", " + FirstMidName;
}
}
}
public class Admin: ApplicationUser
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int AdID;
}
I will also add 2 other inheritance from applicationUser, I want to have separeted models for different users to store different information in their database fileds and let all of them login to my site.
In startup I'm trying to initially add admin user using this code (taken somewhere in stackoverflow answers):
public static async Task CreateRoles(SchoolContext context, IServiceProvider serviceProvider)
{
var userManager = serviceProvider.GetRequiredService<UserManager<ApplicationUser>>();
var roleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();
// First, Creating User role as each role in User Manager
List<IdentityRole> roles = new List<IdentityRole>();
roles.Add(new IdentityRole { Name = "Student", NormalizedName = "STUDENT" });
roles.Add(new IdentityRole { Name = "ADMIN", NormalizedName = "ADMINISTRATOR" });
roles.Add(new IdentityRole { Name = "Professor", NormalizedName = "PROFESSOR" });
//Then, the machine added Default User as the Admin user role
foreach (var role in roles)
{
var roleExit = await roleManager.RoleExistsAsync(role.Name);
if (!roleExit)
{
context.Roles.Add(role);
context.SaveChanges();
}
}
await CreateUser(context, userManager);
}
private static async Task CreateUser(SchoolContext context, UserManager<ApplicationUser> userManager)
{
var adminUser = await userManager.FindByEmailAsync("alpha#lms.com");
if (adminUser != null)
{
if (!(await userManager.IsInRoleAsync(adminUser, "ADMINISTRATOR")))
await userManager.AddToRoleAsync(adminUser, "ADMINISTRATOR");
}
else
{
var newAdmin = new applicationUser()
{
UserName = "alpha#lms.com",
Email = "alpha#lms.com",
LastName = "Admin",
FirstMidName = "Admin",
};
var result = await userManager.CreateAsync(newAdmin, "password123;");
if (!result.Succeeded)
{
var exceptionText = result.Errors.Aggregate("User Creation Failed
- Identity Exception. Errors were: \n\r\n\r",
(current, error) => current + (" - " + error + "\n\r"));
throw new Exception(exceptionText);
}
await userManager.AddToRoleAsync(newAdmin, "ADMINISTRATOR");
}
}
But when I try to do it, I receive an exception:
"Cannot insert the value NULL into column 'Discriminator', table 'aspnet-University; column does not allow nulls. INSERT fails.
The statement has been terminated."
If I try to change variable Admin type on "new Admin" instead of "new applicationUser", I receive another exception:
The entity type 'Admin' was not found. Ensure that the entity type has been added to the model.
I know that this question is about basics of identity framework; I do not understand them well yet; I've just began to understand it's prinicples and I don't know how to handle my problem.
My guess that I need to create New UserManager, which will be able to manipulate instances which inherit from basic applicationUser model. I will be happy if you recommend me relevant resources on this topic and help me solve my problem.
In case if anyone will have this question - I've decided not to use inheritance from ApplicationUser, because it obviously was wrong approach, instead and inherit all custom user models from IdentityUser class. To make this work I've added to StartUp Configure services this lines:
services.AddIdentity <IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<YourContext>()
.AddDefaultTokenProviders();
My user model declared like this:
public class Admin : IdentityUser
{
}
And in context file I have this:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<IdentityUser>(i => {
i.ToTable("Users");
i.HasKey(x => x.Id);
});
modelBuilder.Entity<IdentityRole<>(i => {
i.ToTable("Role");
i.HasKey(x => x.Id);
});
}
With this code I can use Identity's UserManager and RoleManager without additional changes.

ASP.NET MVC 4 error updating entity framework models with related entities

I feel like this should be a pretty common thing to do. I have a model with a related object on it. Let's say it's a User and a user has one Role.
public class User
{
public int Id { get; set; }
public virtual Role Role { get; set; }
/* other stuff that saves fine */
}
public class Role
{
public int Id {get;set;}
public string Name { get;set;}
}
So if I save a new user, or if I edit a user (but don't change his Role), I have no issues. If I have a user without a role, and add a role to him, again no problem (though I manually lookup the role and assign it). If I try and change a role, I get a modelstate error on the Role property that the ID is part of the object's key and can't be changed. So how do folks go about making updates like this? Whitelist the simple values and then manually update the Role?
My controller code in question is here:
[HttpPost]
public ActionResult Save(int id, FormCollection form)
{
var user = data.Users.FirstOrDefault(d=> d.Id == id);
if (user != null)
{
TryUpdateModel(user, form.ToValueProvider());
if (!ModelState.IsValid)
{
var messages = ModelState.Values.Where(m => m.Errors.Count() > 0).SelectMany(m=>m.Errors).Select(e => e.ErrorMessage);
if (Request.IsAjaxRequest())
return Json(new { message = "Error!", errors = messages });
return RedirectToAction("index"); // TODO: more robust Flash messaging
}
updateDependencies(user);
/* negotiate response */
}
}
I'll probably just do it manually for now, but it seems like a scenario that I would have expected to work out of the box, at least to some degree.
Your User model should have a foreign key:
public int? RoleId { get; set; }
public virtual Role Role { get; set; }
You can assign a Role.Id to this value, or make it null when the user does not have a role.
I'm also not sure if your Save function is correct. I'm always using this pattern (not sure if it is correct either...), but of course it depends on the data you post to the server:
[HttpPost]
public ActionResult Save(User model)
{
if (ModelState.IsValid)
{
// Save logic here, for updating an existing entry it is something like:
context.Entry(model).State = EntityState.Modified;
context.SaveChanges();
return View("Success");
}
return View("Edit", model);
}

Cannot get relationship to update for navigation properties in entity framework

I am currently using EF4.3 and Code First. Creation of my objects works (via my views - just using the auto-generated Create), but when I attempt to edit an object, it does not save any changes that, utlimately, tie back to my navigation properties. I have been reading on relationships, but I don't understand how to tell my context that the relationship has changed.
Here is some example code of my implementation.
#* Snippet from my view where I link into my ViewModel. *#
<div class="row">
<div class="editor-label">
#Html.LabelFor(model => model.ManagerID)
</div>
<div class="editor-field">
#Html.DropDownListFor(model => model.ManagerID, ViewBag.Manager as SelectList, String.Empty)
#Html.ValidationMessageFor(model => model.ManagerID)
</div>
</div>
Here is my Controller implementation (POST of my Edit):
[HttpPost]
public ActionResult Edit(ProjectViewModel projectViewModel)
{
if (ModelState.IsValid)
{
Project project = new Project();
project.ProjectID = projectViewModel.ProjectID;
project.Name = projectViewModel.Name;
project.ProjectManager = repository.GetUser(projectViewModel.ManagerID);
repository.InsertOrUpdateProject(project);
repository.Save();
return RedirectToAction("Index");
}
ViewBag.Manager = new SelectList(repository.GetUsers(), "UserID", "FullName", projectViewModel.ManagerID);
return View(projectViewModel);
}
Within my Project object:
public class Project
{
public int ProjectID { get; set; }
[Required]
public string Name { get; set; }
// Navigation Properties
public virtual User Manager { get; set; }
}
Here is the corresponding method from the repository (where my context resides):
public void InsertOrUpdateProject(Project project)
{
if (program.ProjectID == default(int))
{
context.Projects.Add(project);
}
else
{
context.Entry(project).State = EntityState.Modified;
}
}
Just to be clear, this does work to update my properties, but it does not update my navigation properties (in this case, Manager). Appreciate any help.
Setting the state to Modified only marks scalar properties as modified, not navigation properties. You have several options:
A hack (you won't like it)
//...
else
{
var manager = project.Manager;
project.Manager = null;
context.Entry(project).State = EntityState.Modified;
// the line before did attach the object to the context
// with project.Manager == null
project.Manager = manager;
// this "fakes" a change of the relationship, EF will detect this
// and update the relatonship
}
Reload the project from the database including (eager loading) the current manager. Then set the properties. Change tracking will detect a change of the manager again and write an UPDATE.
Expose a foreign key property for the Manager navigation property in your model:
public class Project
{
public int ProjectID { get; set; }
[Required]
public string Name { get; set; }
public int ManagerID { get; set; }
public virtual User Manager { get; set; }
}
Now ManagerID is a scalar property and setting the state to Modified will include this property. Moreover you don't need to load the Manager user from the database, you can just assign the ID you get from your view:
Project project = new Project();
project.ProjectID = projectViewModel.ProjectID;
project.Name = projectViewModel.Name;
project.ManagerID = projectViewModel.ManagerID;
repository.InsertOrUpdateProject(project);
repository.Save();
There are several options here, I will list 3 of them:
Option 1: Using GraphDiff
*This needs the Configuration.AutoDetectChangesEnabled of your context set to true.
Just install GraphDiff with NuGet
Install-Package RefactorThis.GraphDiff
Then
using (var context = new Context())
{
var customer = new Customer()
{
Id = 12503,
Name = "Jhon Doe",
City = new City() { Id = 8, Name = "abc" }
};
context.UpdateGraph(customer, map => map.AssociatedEntity(p => p.City));
context.Configuration.AutoDetectChangesEnabled = true;
context.SaveChanges();
}
For more details about GraphDiff look here.
Option 2: Find and Edit
Searching your entity with EF to track it to the context. Then edit the properties.
*This needs the Configuration.AutoDetectChangesEnabled of your context set to true.
var customer = new Customer()
{
Id = 12503,
Name = "Jhon Doe",
City = new City() { Id = 8, Name = "abc" }
};
using (var context = new Contexto())
{
var customerFromDatabase = context.Customers
.Include(x => x.City)
.FirstOrDefault(x => x.Id == customer.Id);
var cityFromDataBase = context.Cities.FirstOrDefault(x => x.Id == customer.City.Id);
customerFromDatabase.Name = customer.Name;
customerFromDatabase.City = cityFromDataBase;
context.Configuration.AutoDetectChangesEnabled = true;
context.SaveChanges();
}
Option 3: Using a scalar property
In a matter of performance this is the best way, but it mess your class with database concerns. Because you will need to create a scalar (primitive type) property to map the Id.
*In this way there is no need to set the Configuration.AutoDetectChangesEnabled to true. And also you won't need to do a query to the database to retrieve the entities (as the first two options would - yes GraphDiff does it behind the scenes!).
var customer = new Customer()
{
Id = 12503,
Name = "Jhon Doe",
City_Id = 8,
City = null
};
using (var contexto = new Contexto())
{
contexto.Entry(customer).State = EntityState.Modified;
contexto.SaveChanges();
}
I am not sure exactly what you mean by navigation properties? Do you mean like a foreign key relationship? If so then try the following data annotation:
public class Project
{
public int ProjectID { get; set; }
[Required]
public string Name { get; set; }
[ForeignKey("YourNavigationProperty")]
public virtual UserManager { get; set; }
}
Update your EF Context, and see what happens?
UPDATE
public class Project
{
public int ProjectID { get; set; }
[Required]
public string Name { get; set; }
[ForeignKey("ManagerId")]
public ManagerModel UserManager { get; set; }
}
public class ManagerModel
{
[Key]
public int ManagerId { get; set; }
public String ManagerName { get; set; }
}
See if that works?

Entity Framework: Tracking changes to FK associations

I'm overriding SaveChanges on my DbContext in order to implement an audit log. Working with many-to-many relationships or independent associations is relatively easy as EF creates ObjectStateEntries for any changes to those kinds of relationships.
I am using foreign key associations, and when a relationship between entities changes all you get is an ObjectStateEnty that says for example entity "Title" has "PublisherID" property changed. To a human this is obviously a foreign key in Title entity, but how do I determine this in runtime? Is there a way to translate this change to a "PublisherID" property to let's an EntityKey for the entity that foreign key represents?
I assume I'm dealing with entities that look like this:
public sealed class Publisher
{
public Guid ID { get; set; }
public string Name { get; set; }
public ICollection<Title> Titles { get; set; }
}
public class Title
{
public Guid ID { get; set; }
public string Name { get; set; }
public Guid? PublisherID { get; set; }
public Publisher Publisher { get; set; }
}
There is also EF EntityConfiguration code that defines the relationship and foreign key:
public TitleConfiguration()
{
HasOptional<Publisher>(t => t.Publisher).WithMany(
p => p.Titles).HasForeignKey(t => t.PublisherID);
}
What I'm doing now seems a bit too complicated. I'm hoping there is more elegant way to achieve my goal. For every modified property from ObjectStateEntry I look through all ReferentialConstraints for current entity and see if any of those use it as a foreign key. The code below is called from SaveChanges():
private void HandleProperties(ObjectStateEntry entry,
ObjectContext ctx)
{
string[] changedProperties = entry.GetModifiedProperties().ToArray();
foreach (string propertyName in changedProperties)
{
HandleForeignKey(entry, ctx, propertyName);
}
}
private void HandleForeignKey(ObjectStateEntry entry,
ObjectContext ctx, string propertyName)
{
IEnumerable<IRelatedEnd> relatedEnds =
entry.RelationshipManager.GetAllRelatedEnds();
foreach (IRelatedEnd end in relatedEnds)
{
// find foreign key relationships
AssociationType elementType = end.RelationshipSet.ElementType as
AssociationType;
if (elementType == null || !elementType.IsForeignKey) continue;
foreach (ReferentialConstraint constraint in
elementType.ReferentialConstraints)
{
// Multiplicity many means we are looking at a foreign key in a
// dependent entity
// I assume that ToRole will point to a dependent entity, don't
// know if it can be FromRole
Debug.Assert(constraint.ToRole.RelationshipMultiplicity ==
RelationshipMultiplicity.Many);
// If not 1 then it is a composite key I guess.
// Becomes a lot more difficult to handle.
Debug.Assert(constraint.ToProperties.Count == 1);
EdmProperty prop = constraint.ToProperties[0];
// entity types of current entity and foreign key entity
// must be the same
if (prop.DeclaringType == entry.EntitySet.ElementType
&& propertyName == prop.Name)
{
EntityReference principalEntity = end as EntityReference;
if (principalEntity == null) continue;
EntityKey newEntity = principalEntity.EntityKey;
// if there is more than one, the foreign key is composite
Debug.Assert(newEntity.EntityKeyValues.Length == 1);
// create an EntityKey for the old foreign key value
EntityKey oldEntity = null;
if (entry.OriginalValues[prop.Name] is DBNull)
{
oldEntity = new EntityKey();
oldEntity.EntityKeyValues = new[] {
new EntityKeyMember("ID", "NULL")
};
oldEntity.EntitySetName = newEntity.EntitySetName;
}
else
{
Guid oldGuid = Guid.Parse(
entry.OriginalValues[prop.Name].ToString());
oldEntity = ctx.CreateEntityKey(newEntity.EntitySetName,
new Publisher()
{
ID = oldGuid
});
}
Debug.WriteLine(
"Foreign key {0} changed from [{1}: {2}] to [{3}: {4}]",
prop.Name,
oldEntity.EntitySetName, oldEntity.EntityKeyValues[0],
newEntity.EntitySetName, newEntity.EntityKeyValues[0]);
}
}
}
}
I hope this helps to illustrate better what I am trying to achieve. Any input is welcome.
Thanks!
Looks like my code is the right solution to this problem :/
I did end up using independent associations to avoid this problem altogether.