Updating user via UserManager clears navigation property - asp.net-core

I have a user class with a navigation property that acts as the join model for a many-to-many relationship.
As an example:
class User
{
public string Id { get; set; }
public ICollection<UserFoo> Foos { get; set; }
}
class UserFoo
{
public string UserId { get; set; }
public User User { get; set; }
public int FooId { get; set; }
public Foo Foo { get; set; }
}
class Foo
{
public int Id { get; set; }
public ICollection <UserFoo> Users { get; set; }
}
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<UserFoo>
.HasOne(uf => uf.User)
.WithMany(u => u.Foos)
.HasForeignKey(uf => uf.UserId);
builder.Entity<UserFoo>
.HasOne(uf => uf.Foo)
.WithMany(u => u.Users)
.HasForeignKey(uf => uf.FooId);
}
When updating a user with the UserManager class, it exhibits strange behaviour with the navigation property.
The first time I add some relationships to the user, it updates fine as you would expect.
Any subsequent time I make changes, that's when things get weird.
If a user has a link to FooA and FooB, adding a link to FooC will erase FooA and FooB, resulting in only FooC.
If a user has a link to FooA and FooB, removing either of them removes all links.
The weird state comes after calling UserManager.UpdateAsync - the user model passed to it is in the correct state before the call, and the wrong one after the call ends.
I've traced this down to the internal call to Store.UpdateAsync. I can't figure out where to go from there, other than switching to using a context to update the user.
The update method (snipped):
public IActionResult Update(string id, UserForm form)
{
// form in correct state
var user = userManager.Users.Include(u => u.Foos).FirstOrDefault(u => u.Id == id);
// user in correct state
var role = await roleManager.FindByIdAsync(form.RoleId);
mapper.Map(form, user); // AutoMapper
// user now in correct state after mapping
var result = await userManager.UpdateAsync(user, role, form.Password);
// user in incorrect state
}

Related

ASP MVC Entity Framework core. Update many to many relationship on form post

I'd like to know a good practice to update a many to many relationship when submit form.
I got these two entities and I use the default many to many relationship from EF core 5:
public class BlogEntry
{
public int Id { get; set; }
[Required]
[MaxLength(200)]
public string Title { get; set; }
[Required]
public string EntryText { get; set; }
[NotMapped]
public IEnumerable<string> CategoriesToPublish { get; set; }
public ICollection<Category> Categories { get; set; }
}
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<BlogEntry> BlogEntries { get; set; }
}
context:
public DbSet<BlogEntry> BlogEntries { get; set; }
public DbSet<Category> Categories { get; set; }
And I have a form witha multiselect field to represent this relationship. See Image
form
I'm not using the relation property on the form(maube I should, but I don't know), I have another property to convert the relationship into a list of strings called CategoriesToPublish so I can load the multiselect and retrieve the selection on post.
On the post action method, I want to iterate the this CategoriesToPublish and update all the relationships.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Publish(BlogEntry blogEntry)
{
if (ModelState.IsValid)
{
blogEntry.Categories = await _context.Categories.Where(x => x.BlogEntries.Any(x => x.Id == blogEntry.Id)).ToListAsync();
await UpdateCategories(blogEntry);
_context.Update(blogEntry);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(List));
}
return View(blogEntry);
}
But the problem that I'm facing is that the Categories relationship is not loaded on postback. And if I try to load it manually and save context, I get an error saying SqlException: Violation of PRIMARY KEY constraint 'PK_BlogEntryCategory'. Cannot insert duplicate key in object 'dbo.BlogEntryCategory'
I am not sure how to approach this problem. Any advice?
After lot of searching I found out a solution.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Publish(BlogEntry blogEntry)
{
if (ModelState.IsValid)
{
blogEntry = _context.Update(blogEntry).Entity;
blogEntry.Categories = await _context.Entry(blogEntry).Collection(u => u.Categories).Query().ToListAsync();
await UpdateCategories(blogEntry);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(List));
}
return View(blogEntry);
}
I retrieve the blogEntry after Update:
blogEntry = _context.Update(blogEntry).Entity;
At this point the Categories are still empty, but now we can load them from DB again:
blogEntry.Categories = await _context.Entry(blogEntry).Collection(u => u.Categories).Query().ToListAsync();
and boila, now we are good to iterate all the Categories change them if needed, it doesn't complain about duplicate keys.

EF Core tries to add new record instead of update 1:many relation

I'm not sure why while trying to create an entity which is 1:many
EF tries to add new entry in Asp Net Users instead of update 1:many
I have one user which has many items
SqlException: Violation of PRIMARY KEY constraint 'PK_AspNetUsers'. Cannot insert duplicate key in object 'dbo.AspNetUsers'. The duplicate key value is (cdbb1f2f-ddcf-40c0-97ec-f50f8049d87a).
public class Context : IdentityDbContext
{
public Context(DbContextOptions options) : base(options)
{
}
public DbSet<User> Users { get; set; }
public DbSet<Item> Items { get; set; }
public DbSet<File> Files { get; set; }
}
public class User : IdentityUser
{
public List<Item> Items { get; set; } = new List<Item>();
}
public class Item
{
private Item()
{
}
public Item(string title, User owner, File file)
{
Title = title;
Owner = owner;
File = file;
}
public int Id { get; private set; }
public string Title { get; set; }
public User Owner { get; set; }
public File File { get; set; }
public DateTime CreationDate { get; } = DateTime.Now;
}
And here's where's the problem:
var fileResult = await _file.SaveFile(input.File);
var item = new Item(input.Title, user, fileResult.File);
user.Items.Add(item);
await _context.Items.AddAsync(item);
await _context.SaveChangesAsync();
User is loaded with:
public User GetUser()
{
return _context.Users.FirstOrDefault(x => x.UserName == _http.HttpContext.User.Identity.Name);
}
I tried that:
When I change
public Item(string title, User owner, File file)
{
Title = title;
Owner = owner;
File = file;
}
to just:
public Item(string title, File file)
{
Title = title;
File = file;
}
and let it be handled by:
user.Items.Add(item);
then OwnerId in DB is null
Using:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<User>()
.HasMany(x => x.Items)
.WithOne(x => x.Owner);
modelBuilder.Entity<Item>().HasOne(x => x.Owner);
}
is not helping either
The problem was casued by ServiceLifetime of DbContext
Because User was loaded in Controller and then thrawn into Service that was responsible for business logic
I changed
(options => options.UseSqlServer(Configuration["Database:ConnectionString"]), ServiceLifetime.Transient);
to
(options => options.UseSqlServer(Configuration["Database:ConnectionString"]), ServiceLifetime.Scoped);
and it works fine.

Mapping Id to Model for controller API

I'm using asp.net core on a project. (I'm fairly new to it)
I have a User Model. the code below is a simplified version:
public class User
{
public int id { get; set; }
// attribute declaration
public ICollection<User> friends { get; set; }
}
I'm using automapper service to map my api to this Model:
public class UserResource
{
public UserResource()
{
this.friendsId = new List<int>();
}
public int id { get; set; }
// attribute declaration
public ICollection<int> friendsId { get; set; }
}
consider a post request to UserController with the following body:
{
"id" : 1
"friendsId": [2,3,4],
}
I want to map integers in friendsId to id of each user in friends collection. but I can't figure out what to do. here's what I've got:
CreateMap<UserResource,User>()
.ForMember(u => u.friends,opt => opt.MapFrom(????);
is this the right approach? if so how should I implement it?
or should I change my database model to this:
public class User
{
public int id { get; set; }
// attribute declaration
public ICollection<int> friendsId { get; set; }
}
Thank you in advance.
You'll need to implement a custom value resolver. These can be injected into, so you can access things like your context inside:
public class FriendsResolver : IValueResolver<UserResource, User, ICollection<User>>
{
private readonly ApplicationDbContext _context;
public FriendsResolver(ApplicationDbContext context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
}
public ICollection<User> Resolve(UserResource source, User destination, ICollection<User> destMember, ResolutionContext context)
{
var existingFriendIds = destMember.Select(x => x.Id);
var newFriendIds = source.friendsId.Except(existingFriendIds);
var removedFriendIds = existingFriendIds.Except(source.Friends);
destMember.RemoveAll(x => removedFriendIds.Contains(x.Id);
destMember.AddRange(_context.Users.Where(x => newFriendIds.Contains(x.Id).ToList());
return destMember;
}
}
Not sure if that's going to actually work as-is, as I just threw it together here, but it should be enough to get your going. The general idea is that you inject whatever you need into the value resolver and then use that to create the actual stuff you need to return. In this case, that means querying your context for the User entities with those ids. Then, in your CreateMap:
.ForMember(dest => dest.friends, opts => opts.ResolveUsing<FriendsResolver>());
This only covers one side of the relationship, though, so if you need to map the other way, you may need a custom resolver for that path as well. Here, I don't think you actually do. You should be able to just get by with:
.ForMember(dest => dest.friendsId, opts => opts.MapFrom(src => src.friends.Select(x => x.Id));
This would help
CreateMap<UserResource,User>()
.ForMember(u => u.friends,opt => opt.MapFrom(t => new User {FriendsId = t.friendsId);
public class User
{
...
public ICollection<User> friends { get; set; }
}
Where friends is ICollection<User> whereas UserResource class has ICollection<int>. There is type mismatch here. You need to map ICollection to ICollection that is why I casted new User ...

Best pratice for completing MVC model properties which aren't bound?

I am trying to find the best way to use MVC for models which are only partially edited.
Below is an simple example.
Model
using System.ComponentModel.DataAnnotations;
public class SimpleModel
{
public int Id { get; set; }
public string Parent { get; set; }
[Required]
public string Name { get; set; }
}
View
using System.ComponentModel.DataAnnotations;
public class SimpleModel
{
public int Id { get; set; }
public string Parent { get; set; }
[Required]
public string Name { get; set; }
}
Controller
using System.Web.Mvc;
public class SimpleController : Controller
{
public ActionResult Edit(int id)
{ return View(Get(id)); }
[HttpPost]
public ActionResult Edit(int id, SimpleModel model)
{
if (model.Name.StartsWith("Child")) //Some test that is not done client-side.
{
Save(model);
//Get the saved data freshly.
//model = Get(id);
}
else
{
ModelState.AddModelError("", "Name should start with 'Child'");
}
//Is this the way to set the Parent property?
//var savedModel = Get(id);
//model.Parent = savedModel.Parent;
return View(model);
}
//Mock a database.
SimpleModel savedModel;
private void Save(SimpleModel model)
{ savedModel = new SimpleModel() { Id = model.Id, Name = model.Name }; }
private SimpleModel Get(int id)
{
if (savedModel == null)
{ return new SimpleModel() { Id = id, Parent = "Father", Name = "Child " + id.ToString() }; }
else
{ return new SimpleModel() { Id = savedModel.Id, Parent = "Father", Name = savedModel.Name }; }
}
}
The Name field is editable. The Parent field is only for reference and should not be updated. Therefore, it is rendered using DisplayFor.
Upon post, I receive a model with property Parent set to null. That's no problem as it will not be saved. However, when I simply return the received model to the view, the Parent field will no longer be displayed. When the model is valid, I can easily get it again from the database and thus get the Parent field's value back.
When the model is not valid, I would like to allow the user to correct input and attempt to save once more. There, I the received model's values that are input should be used, but the displayed values should be shown as well.
In reality, there are many more fields to be shown for reference, most often from different database entities than the one that is being edited.
I have seen suggestions to pass the fields as hidden fields in the view, but I feel very reluctant to read data from the client that should not be updated.
Is there a more elegant way to do this than copying these values into the model manually or passing them as hidden fields?
What about giving those un-editable properties to another model and let it take care of those properties?
View Model
public class PersonViewModel
{
public int Id { get; set; }
[Required]
public string FirstName { get; set; }
[Required]
public string LastName { get; set; }
public PersonDetailsModel DetailsModel { get; set; }
}
Details Model
public class PersonDetailsModel
{
public string Mother { get; set; }
public string Father { get; set; }
public PersonDetailsModel() { }
public PersonDetailsModel(int personId)
{
// pull required model data from databases
var db = DBParentContext;
Mother = db.Parent.Where(m => m.ChildId == personId)
Father = db.Parent.Where(m => m.ChildId == personId)
}
}
Controller
public class PersonController : Controller
{
[HttpPost]
public ActionResult Edit(PersonViewModel viewModel)
{
viewModel.DetailsModel = new PersonDetailsModel(viewModel.Id)
if (ModelState.IsValid) {
// ~
}
return View(viewModel)
}
}
View
#model PersonViewModel
#Html.DisplayFor(m => m.DetailsModel.Mother)
#Html.DisplayFor(m => m.DetailsModel.Father)
#Html.TextBoxFor(m => m.FirstName)
#Html.TextBoxFor(m => m.LastName)
Since details like your Mother are un-editable then they're not really part of the "Edit" model, so I'd box like that away and try to let something else take care of them.
If you aren't going to update the Parent field, then it really doesn't matter if it's a hidden or not, since you won't update it on post.
I would use the hidden in this case, just make sure not to update that field.

Why my EF collection are not lazy?

I'm using EF 4.1 Code first. I have a model of user and a model of setting.
Each time the repository returns a user the Setting is also loaded. I've marked the Setting as virtual all my access modifiers are public LazyLoadingEnabled and ProxyCreationEnabled are enabled by default.
What am I missing?
public class User : BaseEntity
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public virtual ICollection<Setting> Settings { get; set; }
}
public class Setting
{
public int UserID { get; set; }
public int SettingID { get; set; }
public string Value { get; set; }
}
The User might have several setting, so there is a one to many relationship with a foreign key in setting.
The user configuration is
public class UserConfiguration : EntityTypeConfiguration<User>
{
public UserConfiguration()
{
HasKey(u => u.ID);
HasMany(u => u.Settings).WithOptional().HasForeignKey(u => u.UserID);
}
}
and the Setting configuration is:
public class SettingsConfiguration : EntityTypeConfiguration<Setting>
{
public SettingsConfiguration()
{
ToTable("UserSettings");
HasKey(s => new { s.UserID, s.SettingID });
}
}
Lazy loading means the opposite of what you think it means.
With lazy loading (virtual property and defaults)
Settings is not retrieved immediately when querying User
Settings is retrieved when it's accessed for the first time. The DbContext must be open at that time for this to happen; otherwise you'll get an exception
Without lazy loading (non-virtual property and/or explicitly disabled)
Settings is not retrieved immediately when querying User
Settings will never be retrieved automatically; it will return null (which, in my opinion, is a terrible design decision: null is a wrong value and you shouldn't be able to get it)
In both cases, you can load Settings eagerly by using .Include(x => x.Settings), or when needed, by calling context.Entry(user).Collection(x => x.Settings).Load()