I'm doing a hobby project for myself and later turn it into a portfolio work.
I'm working in Entityframework 6, with razor pages.
My class UserModel is a child of the IdentityUser class. (I presume you know what an IdentityUser, if not)
https://learn.microsoft.com/en-us/aspnet/core/security/authentication/identity-api-authorization?view=aspnetcore-7.0
Inside the UserModel I also stores an Icollection of GroupClass.
My GroupClass has an ICollection of UserModels. Which creates a many to many relationship in EF6.
When i try to store the UserModel object in the groupclass, i get the error: Cannot implicitly convert type to AspNetCore.Identity.IdentityUser to System.collection.Generic.ICollection.
What i understand is, i somehow need to change the class groupClass. To be able to store an identity variable instead of a Icollection usermodel. `
What i don't understand is, why isn't UserModel already a part of IdentityUser, as it's a child and should have the same parameters as it's parent.
My question is, how do i store the UserModel inside my groupclass?
public class GroupClassModel
{
[Key]
public int GroupClassID { get; set; }
// info dump removed
public string userID { get; set; }
public ICollection<UserModel>? userModels { get; set; }
}
public class UserModel : IdentityUser
{
// info dump removed
public int? groupClassID { get; set; }
public ICollection<GroupClassModel>? GroupClass{ get; set; }
}
if (ModelState.IsValid)
{
//info removed
if (newGroup == null)
{
var NewGroup = new GroupClassModel()
{
userModels = await userManager.GetUserAsync(User)
};
Context.groupClass.Add(NewGroup );
Context.SaveChanges();
}
return RedirectToPage("Index");
}
return Page();
public class Context : IdentityDbContext
{
Context(DbContextOptions<Context> options) : base(options)
{
}
public DbSet<GroupClassModel> groupClassModels { get; set; }
public DbSet<UserModel> UserModels { get; set; }
public DbSet<MasterModel> MasterModels { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
}
}
`
I tried changing the Icollection in both classes to Icollection. same problem. I tried changing Icollection To IdentityUser but i got tons of other errors. Now i'm 50/50 if latter is the correct way of doing this.
GroupClassModel.userModels is a collection of UserModels. This line of code is trying to set that property to a single instance of a UserModel.
// this generates an error
userModels = await userManager.GetUserAsync(User);
Instead you need to instantiate a new collection and add the current user's UserModel to that collection. (You're also going to need to cast the return value of userManager.GetUserAsync, which is an IdentityUser, to UserModel.)
userModels = new List<UserModel>();
// currentUser is an IdentityUser
var currentUser = await userManager.GetUserAsync(User);
// cast the IdentityUser to a UserModel
UserModel currentUserModel = (UserModel)currentUser;
userModels.Add(currentUserModel);
Related
I'm doing several things in my method; they're necessary as far as I concerned, but optimizing the code isn't what this question is for.
In this method I'm creating a user, adding the user to a role, creating a Directorate and creating a record in a DirectorateUsers table to link the user to the new Directorate.
There's a few database operations here, so I wanted to try and reduce load on the database by only calling SaveChanges once.
It doesn't seem to be doing anything though; I'm not seeing a new directorate being added and the directorateuser isn't being added either. It creates the user and adds it to the specified role, however.
Is it possible to batch multiple changes to data in Entity Framework this way or do I have to await db.SaveChangesAsync() every time I do something like add or update a record?
[HttpPost]
public async Task<ActionResult> Create([Bind(Include = "MunicipalityId,DirectorateName,UserEmailAddress, UserPassword")] RegisterDirectorateViewModel model)
{
try
{
if (ModelState.IsValid)
{
var user = new ApplicationUser { UserName = model.UserEmailAddress, Email = model.UserEmailAddress };
var createUserResult = await UserManager.CreateAsync(user, model.UserPassword);
if (createUserResult.Succeeded)
{
// Add the user to the directorate role.
await UserManager.AddToRoleAsync(user.Id, nameof(SystemRoles.Directorate));
// Generate the directorate and add the user to it.
var municipality = await db.Municipalities.FindAsync(model.MunicipalityId);
var directorate = new Directorate
{
Action = MetaAction.Create,
ActionBy = user,
ActionDate = DateTime.Now,
Municipality = municipality,
Name = model.DirectorateName
};
db.Directorates.Add(directorate);
var directorateUser = new DirectorateUser
{
Directorate = directorate,
User = user
};
db.DirectorateUsers.Add(directorateUser);
// Expire the token so that it can't be used again.
municipality.TokenExpiryDate = DateTime.Now;
db.Entry(municipality).State = EntityState.Modified;
await db.SaveChangesAsync();
// Sign in the user and redirect to the dashboard.
await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false);
return RedirectToAction("Index", "Dashboard");
}
}
return View(model);
}
catch (Exception ex)
{
TempData["err"] = ex;
return RedirectToAction("Create");
}
}
EDIT
Here's extra models per comments...
public class Directorate
{
public int Id { get; set; }
public string Name { get; set; }
public virtual Municipality Municipality { get; set; }
public ApplicationUser ActionBy { get; set; }
public DateTime ActionDate { get; set; }
public MetaAction Action { get; set; }
}
public class DirectorateUser
{
public int Id { get; set; }
public virtual Directorate Directorate { get; set; }
public virtual ApplicationUser User { get; set; }
}
public class SubdirectorateUser
{
public int Id { get; set; }
public virtual Subdirectorate Subdirectorate { get; set; }
public virtual ApplicationUser User { get; set; }
}
You need many to many relationship. Though you have one User table and Directorates table so DirectorateUsers table contains Many-To-Many relationship between User/ApplicationUser and Directorates. So you have to Customize the Model for Many to Many Relationship.
public class ApplicationUser
{
public ApplicationUser()
{
this.Directorates = new HashSet<Directorate>();
}
....
public virtual ICollection<Directorate> Directorates { get; set; }
}
And the Directorate Model has
public class Directorate
{
public Directorate()
{
this.Users = new HashSet<ApplicationUser>();
}
public virtual ICollection<ApplicationUser> Users{ get; set; }
}
Now the DbContext class looks like...
public class AppDBContext : DBContext
{
public AppDBContext() : base("DefaultConnectionString")
{
}
public DbSet<ApplicationUser> Users{ get; set; }
//or public DbSet<User> Users{ get; set; } //if ApplicationUser doesn't work
public DbSet<Directorate> Directorates{ get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
//Configure a Many-to-Many Relationship using Fluent API
modelBuilder.Entity<User>() //or ApplicationUser
.HasMany<Directorate>(s => s.Directorates)
.WithMany(c => c.Users)
.Map(cs =>
{
cs.MapLeftKey("UserId");
cs.MapRightKey("DirectorateId");
cs.ToTable("DirectorateUser");
});
}
}
This mapping create a good relationship. please check the below link for many to many relationship.
https://www.entityframeworktutorial.net/code-first/configure-many-to-many-relationship-in-code-first.aspx
Now check your code db.SaveChangesAsync(); or without await simply db.SaveChanges()..
Hope this can work.Remember you have to mapping your Object in correct way.
I see that when you are creating the Directorate and the DirectorateUser you are using the "user" variable, which may not be referring the one in the database.
Using the following variable instead of "user" to create the Directorate and the DirectorateUser may solve the issue.
var userDb = await _userManager.FindByNameAsync(user.UserName)
For me, the issue points to something at this.
db.Entry(municipality).State = EntityState.Modified;
await db.SaveChangesAsync();
This will only initiate saving changes related to municipality.
but, I think you should have something like this.
//set all objects that need to be updated in a modified state
db.Entry(municipality).State = EntityState.Modified;
db.Entry(directorate).State = EntityState.Modified;
db.Entry(directorateUser).State = EntityState.Modified;
//finally save all the changes to the database.
await db.SaveChangesAsync();
This is how, I would do it.
I see that you are using UserManager to access an IdentityDbContext. The Identity framework uses an instance of IUserStore to glue the two together. But as you've noticed, every operation immediately saves changes.
The default implementation, UserStore already has a boolean property AutoSaveChanges to prevent saving on every operation, however there doesn't seem to be an obvious way to access this property.
You could either replace the IUserStore service with your own implementation (as per UserManager's AutoSaveChanges in .NET Core 2.1);
public class CustomUserStore : UserStore<IdentityUser>
{
public CustomUserStore(ApplicationDbContext context)
: base(context)
{
AutoSaveChanges = false;
}
}
services.AddScoped<IUserStore<IdentityUser>, CustomUserStore>();
Though you would then need to ensure that all UserManager / SigninManager calls are followed by another explicit save.
Or you could add IUserStore as a dependency, assume that it is an instance of UserStore and change the AutoSaveChanges value around your method;
private UserStore<IdentityUser, IdentityRole, DbContext> store;
public Controller(IUserStore<IdentityUser> store)
{
this.store = store as UserStore<IdentityUser, IdentityRole, DbContext>;
}
public async Task<ActionResult> Create(...){
try{
store.AutoSaveChanges = false;
...
}finally{
store.AutoSaveChanges = true;
}
}
Note that which UserStore generic type you need, depends on which IdentityContext generic type you are using.
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.
This is my view model.
public class ProductViewModel
{
public Guid Id { get; set; }
public string Code { get; set; }
public string Name { get; set; }
public bool IsAvailable { get; set; }
}
When form is posted from client the form is submitted to this Controller
public async Task<IHttpActionResult> AddProduct(ProductViewModel productViewModel)
{
await ServiceInstances.PostAsync("product/add", productViewModel);
return Ok();
}
Then this controller submit the form to the API controller
Which is on my separate Project.
[HttpPost]
[Route("add")]
public IHttpActionResult AddProduct(ProductViewModel model)
{
_productService.AddProduct(model.UserServiceDetails());
return Ok();
}
Extension UserServiceDetails Where i get the Login User Info
public static UserServiceDetailModel<T> UserServiceDetails<T>(this T model)
{
var serviceRequestModel = new ServiceRequestModel<T>()
{
Model = model,
LoginInfo = HttpContext.Current.User.Identity.GetUserLoginInfo();
};
}
AddProductService:
public void AddProduct(UserServiceDetailModel<ProductViewModel> serviceRequestModel)
{
var repo = _genericUnitOfWork.GetRepository<Product, Guid>();
var mapped = _mapper.Map<ProductViewModel, Product>(serviceRequestModel.Model);
mapped.Id = Guid.NewGuid();
mapped.CreatedDate = GeneralService.CurrentDate();
mapped.CreatedById = serviceRequestModel.LoginInfo.UserId;
repo.Add(mapped);
_genericUnitOfWork.SaveChanges();
}
Now my question is Is there any way to assign the value to this field CreatedDate and CreatedById before posting it to service?
Reduce these logic to mapper:
mapped.CreatedDate = GeneralService.CurrentDate();
mapped.CreatedById = serviceRequestModel.LoginInfo.UserId;
Or is there any way that those field gets mapped to Product when
var mapped = _mapper.Map<ProductViewModel, Product>(serviceRequestModel.Model);
Sometime i may have the List<T> on view-model and there i have to add this field using the loop.
So this same mapping may get repeated over and over on Add Method Or Update.
In some entity i have to assign the ModifiedDate and ModifiedById also.
My Mapper Configuration:
public class ProductMapper : Profile
{
public ProductMapper()
{
CreateMap<ProductViewModel, Product>();
}
}
I cannot add the Enitity as IAuditableEntity and Overrride in ApplicationDbContext because my DbContext is in separate Project and i donot have access to Identity there.
I'm currently writing a small test application to understand how IdentityUser works.
I've created a MyUser class that inherits from IdentityUser. The only additional property on my custom user class is a collection of my Book class.
I've created methods on the controller that successfully store new users to the database and associated Books. The problem is when I try to retrieve a user, the Books collection for that user is not populated - it's always null.
When I check the database I can see that a Book is stored in the database with an associated User ID however I can't seem to retrieve this collection.
Here is what I have so far:
Book.cs:
public class Book
{
public int Id { get; set; }
public string Isbn { get; set; }
public string Title { get; set; }
public string Author { get; set; }
}
MyUser.cs:
public class MyUser : IdentityUser
{
public IList<Book> Books { get; set; }
}
MyAppContext.cs:
public class MyAppContext : IdentityDbContext<MyUser>
{
public MyAppContext() : base("MyApp")
{
}
public DbSet<Book> Books { get; set; }
}
AuthRepository:
public class AuthRepository : IDisposable
{
private MyAppContext _ctx;
private UserManager<MyUser> _userManager;
public AuthRepository()
{
_ctx = new MyAppContext();
_userManager = new UserManager<MyUser>(new UserStore<MyUser>(_ctx));
}
public async Task<IdentityResult> RegisterUser(RegistrationModel userModel)
{
MyUser user = new MyUser();
user.UserName = userModel.UserName;
var result = await _userManager.CreateAsync(user, userModel.Password);
return result;
}
public async Task<IdentityResult> UpdateUser(MyUser userModel)
{
var result = await _userManager.UpdateAsync(userModel);
return result;
}
public async Task<MyUser> FindUser(string userName, string password)
{
var user = await _userManager.FindAsync(userName, password);
return user;
}
public async Task<MyUser> GetUser(string userName)
{
var user = await _userManager.FindByNameAsync(userName);
return user;
}
public void Dispose()
{
_ctx.Dispose();
_userManager.Dispose();
}
}
I figured maybe within the GetUser() method I could manually retrieve all books from the Book table with _ctx.Books.Where(b => b.MyUser_id == user.Id) however intellisense isn't even giving me the MyUser_Id property on the Books table.
I'm not really sure how to go about this. All I want to do is load all the associated books for a user automatically but I'm not sure how to do this. Any ideas?
Thanks
Your class for Book doesn't include user information for the foreign key reference. Try adding
[ForeignKey("UserId")]
public MyUser User { get; set; }
to the Book class definition.
When you get the users with the query
_ctx.Users.Include(u=> u.Books)
The books for each user should be included.
As the title says I am using the new C# MVC 5 Identity doing a simple call:
UserManager.AddToRole(User.Id, User.RequestedRole);
I am doing this in a method of my ViewModel that is called from the Controller
The UserManager is created in the Base Class of my ViewModel like this:
UserManager = new UserManager<TMUser>(new UserStore<TMUser>(new TMContext()));
When I make the above call to AddToRole method, I get this Inner Exception (The outer one is generic/useless):
{"A relationship from the 'Ledger_User' AssociationSet is in the 'Deleted' state. Given multiplicity constraints, a corresponding 'Ledger_User_Source' must also in the 'Deleted' state."}
I'm obviously not deleting anything at all but only adding a role to my user. I've had this exception before when I am trying to mix objects from multiple contexts...but I'm not doing that here...please help.
EDIT:
I've gotten rid of the model in case it was interfering and added the following code to my controller:
public ActionResult UpdateRoles(string id)
{
if (ModelState.IsValid)
{
var userManager = new UserManager<TMUser>(new UserStore<TMUser>(new TMContext()));
var userRequestingRole = userManager.FindById(id);
if (!userManager.IsInRole(userRequestingRole.Id, userRequestingRole.RequestedRole))
userManager.AddToRole(userRequestingRole.Id, userRequestingRole.RequestedRole);
// It is still crashing with the same error in the above AddToRole
}
For further information, here is the structure of my TMUser and Ledger objects:
public class TMUser : IdentityUser
{
public TMUser()
{
Ledger = new Ledger();
OrderHistory = new List<Order>();
Clients = new List<Client>();
IsActive = true;
}
[DisplayName("Full Name")]
public string FullName { get; set; }
[DisplayName("Notification Email")]
public string NotificationEmail { get; set; }
public virtual Ledger Ledger { get; set; }
public virtual List<Order> OrderHistory { get; set; }
public virtual List<Client> Clients { get; set; }
public string RequestedRole { get; set; }
public virtual TMUser RoleApprovedBy { get; set; }
public bool IsActive { get; set; }
}
public class Ledger
{
public Ledger()
{
Transactions = new List<Transaction>();
}
public long Id { get; set; }
[Required]
public virtual TMUser User { get; set; }
public virtual List<Transaction> Transactions { get; set; }
public decimal GetBalance()
{
// ...
}
internal void AddTransaction(decimal amount, string description, Order order)
{
// ...
}
}
Another Edit:
Today was another frustrating day. After making some changes in my Context it initially seemed like I fixed the problem. Here is the change I made:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<TMUser>().HasOptional(c => c.RoleApprovedBy);
modelBuilder.Entity<TMUser>().HasOptional(c => c.Ledger);
}
I added the above to the DB Context class, mine is: public class TMContext : IdentityDbContext<TMUser>
This worked the first time, I must have broken some kind of an association? However, when I tried again with a different user, a similar, but slightly different Exception happened:
{"A relationship from the 'TMUser_Ledger' AssociationSet is in the 'Deleted' state. Given multiplicity constraints, a corresponding 'TMUser_Ledger_Target' must also in the 'Deleted' state."}
So it feels like I am back to square one...I can keep going by removing the Ledger from the User object, but that would be cheating...I really don't want to get hacky with it...please help...
The problem is that you create a new Ledger in the constructor of the TMUser, when you do that you will remove the current ledger for the TMUser and replace it with a new empty one. And then, EF will handle the new Ledger as new object that needs to be inserted in the database. Thats why you are getting the validation error about an entity that is en deleted state.
Another thing with creating an new Ledger in the constructor of TMUser causes the effect that every TMUser has a ledger but in your database model you have set it to nullable (bacause of the HasOptional).