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.
Related
READ THE EDIT!
I have two Entities :
public class Principal {
public Guid Id { get; private set; }
public Collection<Dependant> Dependants { get; init; } = new();
public Principal() { }
}
public class Dependant{
public Guid Id { get; private set; }
public Guid PrincpalId { get; private set; }
public Principal Principal{ get; private set; }
public Dependant() { }
}
I access Principal through a repository :
internal class PrincipalsRepository {
private readonly DbSet<Princpal> db;
public PrincipalsRepository (DbSet<Princpal> db) {
this.db = db;
}
public async Task AddAsync(Principal p) {
await this.db.AddAsync(p).ConfigureAwait(false);
}
public async Task<Principal>> GetByIdAsync(Guid id) {
//Notice how there's no Include here!
return await db
.FirstOrDefaultAsync(p => p.Id == id)
.ConfigureAwait(false);
}
}
I configure them like this :
public void Configure(EntityTypeBuilder<Principal > builder) {
builder
.ToTable("Principals")
.HasKey(p => p.Id);
builder
.Navigation(p => p.Dependants)
.AutoInclude(false); //THIS!!!!!
builder
.OwnsMany(p =>
p.Dependants,
navBuilder => {
navBuilder.ToTable("Dependants");
navBuilder.Property<Guid>("Id"); //Important: without this EF would try to use 'int'
navBuilder.HasKey("Id");
navBuilder
.WithOwner(v => v.Principal)
.HasForeignKey(v => v.PrincipalId);
}
);
}
The repo is used in a DbContext:
//PLEASE NOTE: This code might seem a bit broken to you because it's a trimmed down copy-paste from the real code.
public abstract class MyDatabase<TContext> : DbContext
where TContext : DbContext {
public PrincipalsRepository PrincipalsRepository = new PrincipalsRepository (DbPrincipals);
//This is exposed for unit tests
public DbSet<Principal> DbPrincipals { get; set; }
public MyDatabase(DbContextOptions<TContext> options)
: base(options) {
}
}
I configure an in-memory Db :
//PLEASE NOTE: Not everything is detailed here. It's a copy paste from a bigger code base)
private static Database CreateDatabase() {
var _connection = new SqliteConnection("Filename=:memory:");
_connection.Open();
_contextOptions = new DbContextOptionsBuilder<MyDatabase>()
.UseSqlite(_connection)
.Options;
var context = new MyDatabase(_contextOptions);
return context;
}
I run a unit test where I insert an Principal entity with a Dependant:
// Step 1 : Init
using var context = CreateDatabase();
var repo = new PrincipalsRepository(context.DbPrincipals);
// Step 2 : Insertion
var p = new Principal();
p.Dependants.Add(new Dependant());
await context.PrincipalsRepo.AddAsync(p).ConfigureAwait(false);
await context.SaveChangesAsync().ConfigureAwait(false);
// Step 3 : Read back
var p2 = context.PrincipalsRepo.GetByIdAsync(p.Id).ConfigureAwait(false);
And then...
Assert.Empty(p2!.Dependants); //The unit test fails because I can see that the Dependant has been loaded
What am I doing wrong? Why is it loaded despite me saying "AutoInclude(false)" ?
Note: After adding AutoInclude(false), creating a new migration changed the Db's model snapshot, but the migration itself was empty. Is that normal???
EDIT:
Like #DavidG and #Gert Arnold suggested in the comments, apparently I need to instantiate a brand new DbContext to do the test, because EF is somehow smart enough to pick up that p2 is the "same" as p, and... populates its navigation links (i.e. does the auto Include) without me asking?!?
I absolutely don't understand what's the logic here (in terms of behaviour consistency).
When I change the test and query p2 from a brand new DbContext instance, it works as I would expect it. I.e. it does find the Principal (p2) but its Dependants collection is empty.
Is this documented anywhere, in one form or another? Even as an implicit sentence that seems obvious on some Microsoft help page?
I'm using the latest version of ABP from abp.io and have two entities with a many-many relationship. These are:
public class GroupDto : AuditedEntityDto<Guid>
{
public GroupDto()
{
this.Students = new HashSet<Students.StudentDto>();
}
public string Name { get; set; }
public bool IsActive { get; set; }
public virtual ICollection<Students.StudentDto> Students { get; set; }
}
and
public class StudentDto : AuditedEntityDto<Guid>
{
public StudentDto()
{
this.Groups = new HashSet<Groups.GroupDto>();
}
public string Name { get; set; }
public bool IsActive { get; set; }
public virtual ICollection<Groups.GroupDto> Groups { get; set; }
}
I set up the following test to check that I am retrieving the related entities, and unfortunately the Students property is always empty.
public async Task Should_Get_List_Of_Groups()
{
//Act
var result = await _groupAppService.GetListAsync(
new PagedAndSortedResultRequestDto()
);
//Assert
result.TotalCount.ShouldBeGreaterThan(0);
result.Items.ShouldContain(g => g.Name == "13Ck" && g.Students.Any(s => s.Name == "Michael Studentman"));
}
The same is true of the equivalent test for a List of Students, the Groups property is always empty.
I found one single related answer for abp.io (which is not the same as ABP, it's a newer/different framework) https://stackoverflow.com/a/62913782/7801941 but unfortunately when I add an equivalent to my StudentAppService I get the error -
CS1061 'IRepository<Student, Guid>' does not contain a definition for
'Include' and no accessible extension method 'Include' accepting a
first argument of type 'IRepository<Student, Guid>' could be found
(are you missing a using directive or an assembly reference?)
The code for this is below, and the error is being thrown on the line that begins .Include
public class StudentAppService :
CrudAppService<
Student, //The Student entity
StudentDto, //Used to show students
Guid, //Primary key of the student entity
PagedAndSortedResultRequestDto, //Used for paging/sorting
CreateUpdateStudentDto>, //Used to create/update a student
IStudentAppService //implement the IStudentAppService
{
private readonly IRepository<Students.Student, Guid> _studentRepository;
public StudentAppService(IRepository<Student, Guid> repository)
: base(repository)
{
_studentRepository = repository;
}
protected override IQueryable<Student> CreateFilteredQuery(PagedAndSortedResultRequestDto input)
{
return _studentRepository
.Include(s => s.Groups);
}
}
This implements this interface
public interface IStudentAppService :
ICrudAppService< // Defines CRUD methods
StudentDto, // Used to show students
Guid, // Primary key of the student entity
PagedAndSortedResultRequestDto, // Used for paging/sorting
CreateUpdateStudentDto> // Used to create/update a student
{
//
}
Can anyone shed any light on how I should be accessing the related entities using the AppServices?
Edit: Thank you to those who have responded. To clarify, I am looking for a solution/explanation for how to access entities that have a many-many relationship using the AppService, not the repository.
To aid with this, I have uploaded a zip file of my whole source code, along with many of the changes I've tried in order to get this to work, here.
You can lazy load, eagerly load or configure default behaviour for the entity for sub-collections.
Default configuration:
Configure<AbpEntityOptions>(options =>
{
options.Entity<Student>(studentOptions =>
{
studentOptions.DefaultWithDetailsFunc = query => query.Include(o => o.Groups);
});
});
Eager Load:
//Get a IQueryable<T> by including sub collections
var queryable = await _studentRepository.WithDetailsAsync(x => x.Groups);
//Apply additional LINQ extension methods
var query = queryable.Where(x => x.Id == id);
//Execute the query and get the result
var student = await AsyncExecuter.FirstOrDefaultAsync(query);
Or Lazy Load:
var student = await _studentRepository.GetAsync(id, includeDetails: false);
//student.Groups is empty on this stage
await _studentRepository.EnsureCollectionLoadedAsync(student, x => x.Groups);
//student.Groups is filled now
You can check docs for more information.
Edit:
You may have forgotten to add default repositories like:
services.AddAbpDbContext<MyDbContext>(options =>
{
options.AddDefaultRepositories();
});
Though I would like to suggest you to use custom repositories like
IStudentRepository:IRepository<Student,Guid>
So that you can scale your repository much better.
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.
as I wrote in title, I have this code:
public class ApplicationUser : IdentityUser
{
public virtual MapPosition MapPosition { get; set; }
public ApplicationUser()
{
MapPosition = new MapPosition { PositionX = 0, PositionY = 0 };
}
public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager)
{
// Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
// Add custom user claims here
return userIdentity;
}
}
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public DbSet<MapPosition> MapPositions { get; set; }
public ApplicationDbContext()
: base("DefaultConnection", throwIfV1Schema: false)
{
}
public static ApplicationDbContext Create()
{
return new ApplicationDbContext();
}
}
And in my controller I have method that I call from #Ajax.ActionLink in my view:
public string ChangeXPosition()
{
var manager = new UserManager<ApplicationUser>(new UserStore<ApplicationUser>(new ApplicationDbContext()));
// Get the current logged in User and look up the user in ASP.NET Identity
currentUser = manager.FindById(User.Identity.GetUserId());
currentUser.MapPosition.PositionX++;
//manager.Update(currentUser);
Debug.WriteLine("currentUser.MapPosition.PositionX: " + currentUser.MapPosition.PositionX);
return "currentUser.MapPosition.PositionX: " + currentUser.MapPosition.PositionX;
}
I want to save to database changed value of currentUser.MapPosition.PositionX.
I have found numerous ways to solve this problem, but none of them worked with my project.
I tried those solutions:
how-to-update-identityuser-with-custom-properties-using-mvc5-and-entity-framewor
updating-user-data-asp-net-identity
mvc5-applicationuser-custom-properties
UPDATE
Ok so I tried to do as you said, but still doesn't work:
public class ApplicationUser : IdentityUser
{
public virtual MapPosition MapPosition { get; set; }
public ApplicationUser()
{
}
}
This version works, my controller method does change the value in the table.
But it works only for user, that have been already created. When I create a new User, his new MapPositions record is not created.
public class ApplicationUser : IdentityUser
{
private MapPosition _mapPosition;
public virtual MapPosition MapPosition
{
get { return _mapPosition ?? (_mapPosition = new MapPosition()); }
}
public ApplicationUser()
{
}
}
This version doesn't work at all, doesn't change the value in database and doesn't create record in MapPositions when new user is created.
At least you have to invoke SaveChanges method on the current ApplicationDbContext instance after updating the entity instance's fields/properties (currentUser.MapPosition.PositionX in your case):
public string ChangeXPosition()
{
var dbContext = new ApplicationDbContext();
var manager = new UserManager<ApplicationUser>(new UserStore<ApplicationUser>(dbContext));
// Get the current logged in User and look up the user in ASP.NET Identity
currentUser = manager.FindById(User.Identity.GetUserId());
currentUser.MapPosition.PositionX++;
//manager.Update(currentUser);
Debug.WriteLine("currentUser.MapPosition.PositionX: " + currentUser.MapPosition.PositionX);
dbContext.SaveChanges();
return "currentUser.MapPosition.PositionX: " + currentUser.MapPosition.PositionX;
}
If it does not help, then you have to explicitly mark the corresponding entry as Modified in the DbContext's change tracker in the following way:
dbContext.Entry(currentUser.MapPosition).State = EntityState.Modified;
After that the change should occur in a database as you can see by manually checking the data in the corresponding table at the database (by using Visual Studio SQL Server Data Tools extension, SQL Server Management Studio or any other database management tool).
I have a many-to-many relationship (Users to skills), and when I try to associate an existing skill to a user, it always creates a new one.
User:
public class ApplicationUser : IdentityUser
{
public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager)
{
// Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
// Add custom user claims here
return userIdentity;
}
public virtual ICollection<Skill> Skills { get; set; }
}
Skill:
public class Skill
{
public int Id { get; set; }
public string Name { get; set; }
}
OnModelCreating:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<ApplicationUser>()
.HasMany<Skill>(user => user.Skills)
.WithMany();
}
Creating the association:
public ActionResult Skills(SkillsViewModel viewModel)
{
var user = UserManager.FindById(User.Identity.GetUserId());
if(!string.IsNullOrWhiteSpace(viewModel.NewSkill)
&& !user.Skills
.Where(sk => sk.Name == viewModel.NewSkill)
.Any())
{
var foundSkill = this.db.Skills
.Where(sk => sk.Name == viewModel.NewSkill)
.FirstOrDefault();
if(foundSkill != null)
{
user.Skills.Add(foundSkill);
}
else
{
user.Skills.Add(new Skill()
{
Name = viewModel.NewSkill
});
}
}
if(viewModel.SelectedSkillId > 0)
{
var foundSkill = this.db.Skills.Find(viewModel.SelectedSkillId);
user.Skills.Add(foundSkill);
}
this.UserManager.Update(user);
return RedirectToAction("skills");
}
I've stepped through, and verified that I do indeed get a 'foundSkill' from the database, but after I add it to the user, and save the user, the skill associated to the user is not the one I found, but a new one with the same name and different ID.
I figured it out. The UserManager was being loaded with one DbContext, and I was trying to associate a Skill loaded from a different DbContext.
Quick hack to test and fix this was to load the user from the same DbContext as the Skills, update the user, save the DbContext.
Longer term solution would be to ensure that everything uses the same DbContext per request.