Update existing Identity ApplicationUser properties via attaching new ApplicationUser to context - c#

I'm trying to update my existing ApplicationUser, by adding new "non-core" properties e.g. FirstName, LastName. I've extented these properties already via ApplicationUser: IdentityUser, this works overall fine.
Now, what I try to do is create a new ApplicationUser in my controller, and then pass it to repository to update user existing in db, like:
public ActionResult Members_Update(MemberViewModel mvm){
ApplicationUser user = new ApplicationUser();
user.FirstName = mvm.FirstName;
user.LastName = mvm.LastName;
_repo.UpdateApplicationUser(user);
}
In repository ( constructed as _context = new ApplicationDbContext(); ) I do:
public void UpdateAppicationUser(ApplicationUser updatedUser){
_context.Users.Attach(updatedUser);
var entry = _context.Entry(updatedUser);
...
and then I continue via
entry.Property(x => x.AccessFailedCount).IsModified = false;
entry.Property(x => x.EmailConfirmed).IsModified = false;
entry.Property(x => x.LockoutEnabled).IsModified = false;
// same for rest of core identity properties
_context.SaveChanges();
}
I'm trying to do this based on this pattern, so that the db is only hit once (and overall looks like an optimal way), however there seems to be some Identity behaviour preventing this. After Attach the entry is just replacing the original user instead of modifying (so e.g. UserName ends up being null, which is naturally required).

Related

An entity object cannot be referenced by multiple instances of IEntityChangeTracker when I try to create a post

I am trying create a post, I'm not quite sure why I get this error. An entity object cannot be referenced by multiple instances of IEntityChangeTracker, if someone could shed some light for me
This is the code in my Controller
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Create(NewPostModel model)
{
var userId = User.Identity.GetUserId();
var user = await UserManager.FindByIdAsync(userId);
var post = BuildPost(model, user);
await PostService.Instance.Add(post);
return RedirectToAction("Index", "Post", new { id = post.Id });
}
private Post BuildPost(NewPostModel post, ApplicationUser user)
{
var now = DateTime.Now;
var forum = ForumServices.Instance.GetById(post.ForumId);
return new Post
{
Title = post.Title,
Content = post.Content,
Created = now,
Forum = forum,
User = user,
IsArchived = false
};
}
A here is my code in the PostService
public async Task Add(Post post)
{
using (var context = new ApplicationDbContext())
{
context.Posts.Add(post);
await context.SaveChangesAsync();
}
}
When I debug, the controller gets the model and the user, however when it gets to the await PostService.Instance.Add(post); it goes to the PostService then comes back to the await line in the controller then I get the error. I'm not sure what to do...
You get this exception because the forum and user property values belong to another Entity Framework context.
When you create your new Post you assign the Forum property to a forum instance. That instance came from ForumServices.Instance.GetById and is associated to a context. Then you add the Post object to another context, the forum instance is now associated to 2 contexts which is not possible.
To fix this, you can change the architecture to use a single EF context per request. It looks like you use a singleton (ForumServices.Instance) if you use .net core, you should have a look at how dependency injection works and use the AddScoped method to have a single EF context for the whole life time of the request. This is the solution I highly recommend.
If it is not possible to change the way context are created you can assign Id instead of full object
return new Post
{
Title = post.Title,
Content = post.Content,
Created = now,
ForumId = forum.ForumId,
UserId = user.UserId,
IsArchived = false
};
If it is still not possible you can return untracked entities. You can do this by using the AsNoTracking method or by using the QueryTrackingBehavior property of the context
context.Forums.AsNoTracking().First(f => f.ForumId == forumId);
// or
context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
Thank you so much, I did as suggested by adding a ForumId and UserId to the models
return new Post
{
Title = post.Title,
Content = post.Content,
Created = now,
ForumId = post.ForumId,
UserId = user.Id,
IsArchived = false
};

Validation failed for one or more entities issue

This is truly one of the strangest issues I've run into.
I have a Web API which uses EF. I have an audit table which takes an ApplicationUser. I create the new object, add it to the collection and then call SaveChangesAsync(). The weird part is, I get "User name MyUserName is already taken." error.
using (var context = new ApplicationDbContext())
{
var user = context.Users.Single<ApplicationUser>(x => x.UserName == model.UserName);
var sid = context.SessionIds.FirstOrDefault(x => x.Id == model.SessionId);
var audit = new Audit
{
Data = model.Data,
User = user,
IpAddress = Helper.GetClientIp(Request),
Session = sid != null ? sid : ItsMyChance.Entities.Entities.SessionId.Create(scoreModel.UserName, scoreModel.GameId)
};
context.Audits.Add(audit);
await context.SaveChangesAsync();
}
Update
This code has been working for years. The difference is I upgrade from .NET 4.5 to .NET 4.61
Update 2
I also tried the following but still receive the same error
[ForeignKey("User")]
public string UserId { get; set; }
public ApplicationUser User { get; set; }
Update 3
Trying to track this issue down I call
var entries = context.ChangeTracker.Entries();
It returns several entries, 1 for each object, including User. User shows Added and another as Unchanged. I can't figure out how this is happening.
In addition, I added the following before making any changes but there's no effect.
context.Configuration.AutoDetectChangesEnabled = false;
Since You are adding the complete user object in Audit , so SaveChangesAsync will save a new Entry for Audit and User also and since a user with same username already exists that's why you are getting this error. What you should do is just assign just the UserId (Whatever is referral key in Audit table for User) in Audit object
var audit = new Audit
{
Data = model.Data,
UserId = user.Id,
IpAddress = Helper.GetClientIp(Request),
Session = sid != null ? sid : ItsMyChance.Entities.Entities.SessionId.Create(scoreModel.UserName, scoreModel.GameId)
};

How to run code within Seed method during deployment?

I have a Web Api solution that makes use of ASP.NET Identity (v2.1) and Entity Framework v6.1. Inside the Seed() method of the Configuration.cs file I have code that creates my first Identity user. This code makes use of the Identity framework to hash the password, create the security stamp, etc. These are all things I cannot do via SQL so adding to the Up() method does not seem like an option.
protected override void Seed(ApplicationDbContext context)
{
// Create the admin user
var manager = new UserManager<ApplicationUser>(new UserStore<ApplicationUser>(new ApplicationDbContext()));
var roleManager = new RoleManager<IdentityRole>(new RoleStore<IdentityRole>(new ApplicationDbContext()));
var user = new ApplicationUser()
{
UserName = "adminuser",
Email = "adminuser#mycompany.com",
EmailConfirmed = true,
FirstName = "John",
LastName = "Does",
JoinDate = DateTime.Now.AddYears(-1)
};
manager.Create(user, "SuperSecurePassword321!");
if (roleManager.Roles.Count() == 0)
{
roleManager.Create(new IdentityRole { Name = "Admin" });
roleManager.Create(new IdentityRole { Name = "Employee" });
roleManager.Create(new IdentityRole { Name = "Customer" });
}
var adminUser = manager.FindByName("adminuser");
manager.AddToRoles(adminUser.Id, new string[] { "Admin" });
}
I need to use FTP to publish this (no control over this). Any suggestions on how to run this code once it is deployed and the database is schema is setup?
Options I have considered:
I have thought about creating an API endpoint that when called can
kick off this code, however, this endpoint would have to allow
anonymous access since it would be creating this first user and
the roles used in the system. I would then need to somehow disable
or remove this endpoint later.
Script the database and include the data and then restore that to
the database server targeted for deployment.
Seed() gets called when the database is accessed the first time. What's wrong with that automatic behavior?
If you want to call it manually, try something like this in protected void Application_Start():
Database.SetInitializer(new YourInititalizer());
var dbContext = new TheContextYouAreUsing();
dbContext.Database.Initialize(force: true);

Seed User with custom role in EF6 code-first

I am having trouble figuring out how to seed additional users and roles into my MVC5 application, using EF6 code first. In order to debug the Seed method from the Configure.cs since update-database was not working, I wrote this controller,
public ActionResult test() {
var context = new ApplicationDbContext();
var roleStore = new RoleStore<IdentityRole>(context);
var roleManager = new RoleManager<IdentityRole>(roleStore);
roleManager.Create(new IdentityRole { Name = "basic" });
var userStore = new UserStore<ApplicationUser>(context);
var userManager = new UserManager<ApplicationUser>(userStore);
var adminthere = context.Users.Any(n => n.UserName == "Admin");
var basicthere = context.Users.Any(n => n.UserName == "Basic");
// Create Dummy basic account
if (!basicthere) {
var basicUser = new ApplicationUser { UserName = "Basic" };
userManager.Create(basicUser, "test");
var _id = basicUser.Id;
userManager.AddToRole(basicUser.Id, "basic");
}
return View();
}
The debugger throws an exception at the userManager.AddToRole(basicUser.Id, "basic"); call saying "UserID not found"? Here is a screenshot including variable values from the debug session:
What is the problem? Also, the exact same code (changing the words "basic" for "Admin") works for seeding the database with the Admin user in role "admin". Why?
EDIT EDIT: moved edit I posted here previoulsy to a real answer below.
As the comments suggested I will post my this as an answer:
The line of code userManager.Create(basicUser, "test"); didn't succeed - the passwort must at least have 6 characters. So while creating the basicUser ApplicationUser instance worked (and hence the _id was not null) I didn't have an IdentityUser of that _id. On admin it succeeded previously bc. I had a different pwd that I didn't want to post here ...

How to delete users that were created with UserManager.CreateAsync

Using asp.net mvc5, my user management systems seems to work. I can login with google or with name/password..
but now I am working on a user management interface in which I need to be able to delete existing users. And this is starting to expose to me just how confusing the user management system is. There's so many different ways to deal with users.. and some of them don't work.
Most everywhere I read, it is talking about using the Membership.DeleteUser().
But that isn't working...
The users were created with.
var user = new ApplicationUser()
{
UserName = model.UserName,
Email = model.Email,
ConfirmationToken = confirmationToken,
IsConfirmed = false
};
var result = await UserManager.CreateAsync(user, model.Password);
Now later on.. how do I delete such a user? (given its name or userid)
I have tried what comes up most on various searches.. comes up with Membership as the solution. But this surely isn't right for MVC5?
For example
var allusers = Membership.GetAllUsers(); // allusers is empty
bool success = Membership.DeleteUser(model.name); // <-- success = false
I can get all the users using this method..
ApplicationDbContext db = new ApplicationDbContext();
foreach (var user in db.Users) { ... }
And I can find an individual user with..
ApplicationDbContext db = new ApplicationDbContext();
var um = new UserManager<ApplicationUser>(new UserStore<ApplicationUser>(db));
ApplicationUser user = um.FindById(model.userId);
Now how do I delete one though? ....
Update
As of Microsoft.AspNet.Identity Version 2.0.0.0, you can now delete users with Identity using UserManager.Delete(user);.
For Posterity
You are referring to two different things, Identity and Membership. Newer versions of ASP.NET support Identity and Membership with Identity being the default, while older versions support only Membership (out of those two authentication systems).
When you create a user with UserManager.CreateAsync, you are doing so within the Microsoft.AspNet.Identity namespace. When you are attempting to delete a user with Membership.DeleteUser, you are doing so within the System.Web.Security namespace. They are living in two different worlds.
As another comment mentions, deleting users is not yet supported out of the box by Identity, but it is the first item on their roadmap for a Spring of 2014 release.
But why wait? Add another property to the ApplicationUser model like this:
public class ApplicationUser : IdentityUser
{
public string IsActive { get; set; }
}
Then, in your controller for deleting a user:
user.IsActive = false;
Do a check when the user logs in:
if (user.IsActive == false)
{
ModelState.AddModelError(String.Empty, "That user has been deleted.");
return View(model);
}
When an deleted user attempts to re-register, instead of UserManager.Create, use UserManager.Update with their new information on the registration page.
These steps will effectively delete the user. If you truly must clear their information from your database, you can use Entity Framework to do that more directly.
added to the previous response. If you have
public class ApplicationUser : IdentityUser
{
public string IsActive { get; set; }
}
Then, in your controller for deleting a user:
user.IsActive = false.ToString();
because your data type is a string and n ot a boolean

Categories

Resources