I have the following architecture:
Standard ASP.NET Identity tables, user can have one and only one role
There are 2 roles : Driver and Manager
User has driver or manager role. Every driver has record in Driver table, Every manager has record in Manager table.
Both, Manager and Driver tables, has FirstName and LastName
My entities and map among them (0..1:1):
public class AspNetUser
{
//.....
public virtual Manager Manager { get; set; }
public virtual Driver Driver { get; set; }
}
public partial class Driver
{
public int Id { get; set; }
//....
public virtual AspNetUser AspNetUser { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
public class Manager
{
public int Id { get; set; }
//....
public virtual AspNetUser AspNetUser { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Nickname { get; set; }
}
public class AspNetUserMap : EntityTypeConfiguration<AspNetUser>
{
public AspNetUserMap()
{
this.HasKey(dp => dp.Id);
this.Property(p => p.UserName).IsRequired().HasMaxLength(256);
this.HasIndex(p => p.UserName).IsClustered(false).IsUnique();
this.HasOptional(c => c.Driver)
.WithOptionalPrincipal(a => a.AspNetUser)
.Map(m => m.MapKey("AspNetUserId"));
this.HasOptional(c => c.Manager)
.WithOptionalPrincipal(a => a.AspNetUser)
.Map(m => m.MapKey("AspNetUserId"));
Now I need to get all users from AspNetUser, but with their FN/LN. To get FN/LN I should check role of user and then call Driver or Manager.
I have the following code:
List<ChatUserDomain> returnedUsers = new List<ChatUserDomain>();
var users = (from i in db.AspNetUsers
.Include(path => path.Driver)
.Include(path => path.Manager)
.Include(path => path.AspNetRoles)
where i.CompanyId == db.AspNetUsers.Where(p => p.UserName.Equals(username, StringComparison.CurrentCultureIgnoreCase)).FirstOrDefault().CompanyId
select new
{
AspNetUser = i,
Username = i.UserName,
....
}
).ToList();
foreach (var user in users)
{
returnedUsers.Add(new ChatUserDomain()
{
UserId = user.AspNetUser.Id,
Name = GetNameByUser(user.AspNetUser),
...
});
}
return returnedUsers;
and GetNameByUser method:
private string GetNameByUser(AspNetUser aspNetUser)
{
IProfile profile = GetProfileByUser(aspNetUser);
if (!String.IsNullOrWhiteSpace(profile.FirstName) && !String.IsNullOrEmpty(profile.LastName))
{
return (GetFirstLettersOfFullName(profile.FirstName, profile.LastName));
}
else
{
return (aspNetUser.UserName.Substring(0, 2));
}
}
private IProfile GetProfileByUser(AspNetUser aspNetUser)
{
IProfile profile = null;
var role = aspNetUser.AspNetRoles.FirstOrDefault().Name;
if (role.Equals(Domain.StaticStrings.RoleStaticStrings.ROLE_DRIVER))
profile = aspNetUser.Driver;
else
profile = aspNetUser.Manager;
return profile;
}
It works, but it creates additional user.Count calls to DB. How to say to EF "load Manager and Driver data in the parent request"?
Related
I have a method that creates a Product. Im getting such a user and checking if he is not null. If there is a user I'm creating new Product. I want to create Product, transfer this product to product dto and add this to the User Product List and return new ProductDto array within product name, product price and the name and Id of the user who created it. But when I try to add the product I'm getting error that I can't convert productDto to product. What I am doing wrong? Any suggestions?
public async Task<ActionResult<ProductDto>> AddProduct(string username, ProductDto productDto)
{
User user= await _context.Users
.Where(x=>x.UserName==username)
.Include(x => x.Products)
.SingleOrDefaultAsync();
if(user!= null)
{
var product = new Product
{
Name = productDto.Name,
Price = productDto.Price,
UserId = u.Id
};
var productToReturn = _mapper.Map<Product, ProductDto>(product);
user.Products.Add(productToReturn); // Cannot convert productDto to product
await _context.SaveChangesAsync();
}
return Ok(productToReturn);
public class ProductDto
{
public string Name { get; set; }
public string Username { get; set; }
public decimal Price { get; set; }
}
public class Product : BaseEntity
{
public string Name { get; set; }
public decimal Price { get; set; }
public User User { get; set; }
public int UserId { get; set; }
}
public class User : BaseEntity
{
public string UserName { get; set; }
public byte[] PasswordHash { get; set; }
public byte[] PasswordSalt { get; set; }
public List<Product> Products { get; set; }
}
I also configured automapper profiles
public AutoMapperProfiles()
{
CreateMap<Product, ProductDto>().ForMember(dest => dest.Username, opt =>
opt.MapFrom(src => src.User.UserName));
}
Seems the issue could be, you are trying to add ProductDto to the user.Products list. But it's looking for an object Product. try to modify code as,
if(user!= null)
{
var product = new Product
{
Name = productDto.Name,
Price = productDto.Price,
UserId = u.Id
};
user.Products.Add(product);
await _context.SaveChangesAsync();
var productToReturn = _mapper.Map<Product, ProductDto>(product);
return Ok(productToReturn);
}
I am populating my context for my tests. but for some reason my Guids gets overwritten upon creation of the entity.
I think there is something wrong on my context setup, but I am below average when it comes to entity framework setups.
Edit:
Seed Code:
try
{
using (var context = this.GetContext())
{
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
var lets = new Entities.Models.Users()
{
Id = Guid.Parse("3859e4c1-aaf7-4d9b-bc5e-8730ae9ad531"),
Name = "Test Pilot",
Organization = new Entities.Models.Organizations
{
Id = 12312
},
AppRoles = new Entities.Models.UserRequestRoles
{
AppRole = ""
}
};
context.Users.Add(lets);
context.SaveChanges();
}
}
catch (Exception e)
{
throw e;
}
User Entity:
public class Users
{
[Key]
[Column("Id")]
public Guid Id { get; set; }
// User Details
public string GivenName { get; set; }
public string Surname { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public string Status { get; set; }
public virtual Organizations Organization { get; set; }
[ForeignKey("Id")]
public virtual UserRequestRoles AppRoles { get; set; }
[ForeignKey("UserId")]
public virtual IEnumerable<UserOrganizations> UserOrganizations { get; set; }
}
Context builder:
builder.Entity<Users>(a =>
{
a.HasKey(a => a.Id);
a.HasOne(b => b.Organization)
.WithMany(b => b.Users);
a.HasMany<UserOrganizations>(c => c.UserOrganizations);
a.HasOne(b => b.AppRoles);
});
There are no errors. its just populating my context.
Any idea why is this happening, thank you
Any idea why is this happening
It's happening because you instructed EF to automatically populate the value of that column via the .HasKey() extension method. Remove the call to that method - you've already specified that the column is a key column with the [Key] attribute defined on it, there's no need to tell EF twice.
In your test, you then need to configure your entity so that it ignores the [Key] attribute:
builder.Entity<Users>(a =>
{
a.HasOne(b => b.Organization)
.WithMany(b => b.Users);
a.HasMany<UserOrganizations>(c => c.UserOrganizations);
a.HasOne(b => b.AppRoles);
}).Property(user => user.Id)
.ValueGeneratedOnAddOrUpdate()
.Metadata.SetAfterSaveBehavior(PropertySaveBehavior.Save);
I am developing a mini social media web app and I use ASP.NET Identity to create and manage user accounts.
I want to add another user account as a friend to my account. I could succesfully do that but the problem is when I checked my added friend's account, there is no update in his friends list. It's empty.
Here is my User class inherited from IdentityUser,
public class AppUser : IdentityUser
{
public AppUser()
{
this.Friends = new HashSet<AppUser>();
}
public int Age { get; set; }
public string Sex { get; set; }
public string City { get; set; }
public string Education { get; set; }
public string Description { get; set; }
public string? FriendOfUserId { get; set; }
public virtual AppUser FriendOf { get; set; }
public ICollection<AppUser> Friends { get; set; }
}
My DbContext class,
public class ApplicationDbContext : IdentityDbContext<AppUser>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<AppUser>(x =>
{
x
.HasMany(x => x.Friends)
.WithOne(x => x.FriendOf)
.HasForeignKey(x => x.FriendOfUserId);
});
}
public DbSet<AppUser> Users { get; set; }
}
My Controller Class method to add friend,
public async Task<IActionResult> AddFriend(string id)
{
var addFriend = await context.Users.Include(u => u.Friends).FirstOrDefaultAsync(u => u.Id == id);
var user = await userManager.GetUserAsync(this.User);
var u = await context.Users.Include(u => u.Friends).FirstOrDefaultAsync(u => u.Id == user.Id);
user.FriendOf = addFriend;
user.Friends.Add(addFriend);
await context.SaveChangesAsync();
return Redirect("/");
}
I think you're not modeling your entity correctly. Since an user can have a list of friends, and also be a friend of other users, I guess you need to capture the latter part in the model.
Since this is a many-to-many relationship, and EF Core still hasn't supported it without declaring an entity to represent the join table, you need to defind that entity as well:
public class AppUser : IdentityUser
{
public int Age { get; set; }
public string Sex { get; set; }
public string City { get; set; }
public string Education { get; set; }
public string Description { get; set; }
public ICollection<AppUserFriendship> FriendsOf { get; set; }
public ICollection<AppUserFriendship> Friends { get; set; }
}
public class AppUserFriendship
{
public string UserId { get; set; }
public AppUser User { get; set; }
public string UserFriendId { get; set; }
public AppUser UserFriend { get; set; }
}
And then you need to configure their relationships:
public class ApplicationDbContext : IdentityDbContext<AppUser>
{
...
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<AppUserFriendship>(b =>
{
b.HasKey(x => new { x.UserId, x.UserFriendId };
b.HasOne(x => x.User)
.WithMany(x => x.Friends)
.HasForeignKey(x => x.UserId)
.OnDelete(DeleteBehavior.Restrict);
b.HasOne(x => x.UserFriend)
.WithMany(x => x.FriendsOf)
.HasForeignKey(x => x.UserFriendId)
.OnDelete(DeleteBehavior.Restrict);
});
}
public DbSet<AppUser> Users { get; set; }
}
Note OnDelete(DeleteBehavior.Restrict). You have to set it to something other than DeleteBehavior.Cascade which is the default to prevent the cascade deletion.
Disclaimer: I wrote all by hand. Never test it.
You will need to use the Include function.
// What you have is fine.
var friend = context.Users.Select ( u => u == id );
// this is what needs to occur in await userManager.GetUserAsync(this.User);
// Without the include the Navigation Property will not be tracked.
var user = context.Users
.Select ( u => u == id )
.Include ( u => u.Friends );
user.Friends.Add ( friend );
context.SaveChanges ();
Check out loading related data.
https://learn.microsoft.com/en-us/ef/core/querying/related-data
EDIT:
Take a look at this post it is a duplicate of this one.
Many-to-many self referencing relationship
EDIT 2:
So the issue you are having is that you are still trying to model this as one table. Think about how this would be structured in a SQL database. How could a single table contain a collection (Friend) in a single column? To accomplish this we will create a new table to model the relationship between AppUsers.
public class Friendship
{
// Foreign Key
Int32 MeId { get; set; }
// Foreign Key
Int32 FriendId { get; set; }
// Navigation Property
AppUser Me { get; set; }
// Navigation Property
AppUser Friend { get; set; }
}
public class AppUser : IdentityUser
{
public AppUser()
{
this.Friends = new HashSet<AppUser>();
}
// Primary Key. Is this defined in IdentityUser?
public int Id { get; }
public int Age { get; set; }
public string Sex { get; set; }
public string City { get; set; }
public string Education { get; set; }
public string Description { get; set; }
// This is considered a Navigation Property
public ICollection<Friendship> Friends { get; set; }
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<AppUser>(x =>
{
x.HasPrimaryKey ( x => x.Id );
});
builder.Entity<Friendship>( f =>
{
f.HasKey ( f => new { f.MeId, f.FriendId } );
f
.HasOne( f => f.Me )
.WithMany ( u => u.Friends )
.HasForeignKey( f => f.MeId );
f
.HasOne( f => f.Friend )
.WithMany ( u => u.Friends )
.HasForeignKey( f => f.FriendId );
});
}
At this point you should be able to query the join table for friendships.
public void AddFriend ( AppUser user, AppUser friend ) {
var trackedUser = context.AppUsers
.Select ( u => u.Id == user.Id )
.Include ( u => u.Friends );
.FirstOrDefault ();
trackedUser.Friends.Add ( new Friendship () {
MeId = user.Id,
FriendId = friend.Id
});
context.SaveChanges ();
}
I have a Many to Many relationship defined between my custom class (Department) and my User class based on Identity 2 defined as below. This generates an extra table, which is all good. However this table is never populated, and from my Profiler i can see that it never creates and executes the SQL to insert the row. What am i doing wrong?
public class Department
{
public int DepartmentId { get; set; }
[DisplayName("Navn")]
public string Name { get; set; }
[DisplayName("Mindst acceptable antal skemaer")]
public int ApprovalThreshold { get; set; }
public List<ATVisit> Atvisits { get; set; }
public bool IsActive { get; set; }
public ICollection<Actionplan> ActionPlans { get; set; }
public int? QuestionSheetTemplateIdFK { get; set; }
[ForeignKey("QuestionSheetTemplateIdFK")]
public QuestionSheetTemplate QuestionSheetTemplate { get; set; }
public virtual List<User> Users { get; set; }
}
public class User : IdentityUser
{
public virtual List<Department> Departments { get; set; }
public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<User> 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;
}
}
I even tried with Fluent API:
modelBuilder.Entity<User>()
.HasMany(x => x.Departments)
.WithMany(c => c.Users);
modelBuilder.Entity<Department>()
.HasMany(x => x.Users)
.WithMany(c => c.Departments);
I am using repositories, and because of this i have to find the User in the Department Repository, otherwise i'll get an error telling me it comes from 2 different Contexts. This is my Department Repository method for updating:
public bool FindUserAndAddToDepartment(Department entity, string userId)
{
var user = context.Set<User>().Where(x => x.Id == userId).FirstOrDefault();
entity.Users.Add(user);
return true;
}
This is the method that handles everything
public ActionResult EditUser(UserViewModel model)
{ //vi starter med at fjerne alle Claims der er department
List<Department> depList = new List<Department>();
var user = UserManager.FindById(model.Id);
if (user.Departments == null)
user.Departments = new List<Department>();
foreach (var selectedDepartment in model.SelectedDepartments)
{
depList.Add(departmentRepo.GetById(selectedDepartment));
}
foreach (var d in depList)
{
if (user.Departments.Where(x => x.DepartmentId == d.DepartmentId).SingleOrDefault() == null)
{
departmentRepo.FindUserAndAddToDepartment(d, user.Id);
departmentRepo.Update(d);
}
}
departmentRepo.Commit();
}
I'm trying to populate my code first database with an admin user account for myself so that I can access the system.
To do this, I'm calling a Seed method in my Global.asax file which does the following:
public void Seed()
{
if (!Roles.Any())
{
List<Role> roles = new List<Role>
{
new Role { RoleName = "Admin" },
new Role { RoleName = "User" }
};
foreach (Role r in roles)
{
Roles.Add(r);
}
SaveChanges();
}
if (!Users.Any())
{
User u = new User();
u.EmailAddress = "my#email.address";
u.Username = "ortund";
u.Password = Hashing.CreateHash("p455W0rd");
u.Role = Roles.Single(r => r.RoleName == "Admin");
Users.Add(u);
SaveChanges();
}
}
User and Role are defined as follows:
namespace Logan.Web.Objects
{
public class User : LoganBaseObject<User>
{
public string Username { get; set; }
public string EmailAddress { get; set; }
public string Password { get; set; }
public string Biography { get; set; }
public virtual Role Role { get; set; }
public virtual ICollection<Article> Articles { get; set; }
public User()
{
Username = String.Empty;
EmailAddress = String.Empty;
Password = String.Empty;
Biography = String.Empty;
Articles = new List<Article>();
}
}
}
public class Role : LoganBaseObject<Role>
{
public string RoleName { get; set; }
public virtual ICollection<User> Users { get; set; }
public Role()
{
RoleName = String.Empty;
Users = new List<User>();
}
}
}
namespace Logan.Web.DBContext
{
public class RoleDBContext : LoganDBBaseObject<Role>
{
private static WebDBContext db = new WebDBContext();
public RoleDBContext()
: base()
{
Property(p => p.RoleName)
.HasColumnName("sRoleName")
.IsRequired();
HasMany(m => m.Users)
.WithRequired();
ToTable("Roles");
}
}
public class UserDBContext : LoganDBBaseObject<User>
{
private static WebDBContext db = new WebDBContext();
public UserDBContext()
: base()
{
Property(p => p.Username)
.HasColumnName("sUsername")
.HasMaxLength(20)
.IsRequired();
Property(p => p.EmailAddress)
.HasColumnName("sEmailAddress")
.HasMaxLength(200)
.IsRequired();
Property(p => p.Password)
.HasColumnName("sPassword")
.HasMaxLength(255)
.IsRequired();
Property(p => p.Biography)
.HasColumnName("sBiography")
.HasColumnType("text");
HasRequired(r => r.Role)
.WithMany(m => m.Users)
.Map(x => x.MapKey("fkRoleID"))
.WillCascadeOnDelete(false);
Property(p => p.CreateDate)
.HasColumnType("datetime");
ToTable("Users");
}
}
}
When the Seed method above gets to SaveChanges(), I get the following error:
An error occurred while saving entities that do not expose foreign key properties for their relationships. The EntityEntries property will return null because a single entity cannot be identified as the source of the exception. Handling of exceptions while saving can be made easier by exposing foreign key properties in your entity types. See the InnerException for details.
Obviously this has something to do with the relationships that are being constructed here, but I don't actually know what I'm doing wrong.
Can anyone recommend a solution and/or explain what the problem is here?
Thanks in advance!
EDIT
Here's a screenshot of the structure that is genergated. The PKey field is added from LoganBaseObject<T>:
I really need to learn to include absolutely all the information from the tables.
I had some datetime columns in the User table for which I wasn't supplying values in the Seed method. See here:
public class User : LoganBaseObject<User>
{
public string Username { get; set; }
public string EmailAddress { get; set; }
public string Password { get; set; }
public string Biography { get; set; }
public virtual Role Role { get; set; }
public bool PasswordReset { get; set; }
public string PasswordResetKey { get; set; }
public DateTime ResetExpiry { get; set; }
public string CreatedBy { get; set; }
public DateTime CreateDate { get; set; }
}
I still don't get why missing values would produce this error about foreign keys, but supplying values in the Seed method fixed the problem.