EF Core: many-to-many query - sql

I am trying to return all items in UserDTO from a User given a particular Team. The below code only returns the UserId (since that is what contained in TeamUser). Can this be done in a straightforward way? Or do I have to perform a second lookup on Users once the UserId's are given?
public class User
{
public long UserId { get; set; }
public string? Name { get; set; }
[Required]
[EmailAddress]
public string? Email { get; set; } = default!;
[Required]
public string? CreatedDate { get; set; }
/* relationships */
public virtual ICollection<TeamUser>? Teams { get; set; } //many Teams to many Users
}
public class Team
{
public long TeamId { get; set; }
[Required]
public string? Name { get; set; }
[Required]
public string? CreatedDate { get; set; }
/* relationships to User */
public virtual ICollection<TeamUser>? Users { get; set; } //many Users to many Teams
}
public class UserDTO
{
public long UserId { get; set; }
public string? Name { get; set; }
[Required]
[EmailAddress]
public string? Email { get; set; } = default!;
}
public class TeamUser
{
public long TeamId { get; set; }
public Team? Team { get; set; }
public long UserId { get; set; }
public User? User { get; set; }
}
[HttpGet]
public async Task<ActionResult<IEnumerable<UserDTO>>> GetUsers(long? userId, long? teamId)
{
if(teamId == null)
{
return await _context.Users.Select(x => UserToDTO(x)).ToListAsync();
}
else
{
// get users given teamId
return await _context.TeamUsers
.Include(u => u.User)
.Where(t => t.TeamId == teamId)
.Select(pt => new UserDTO
{
UserId = pt.UserId
})
.ToListAsync();
}

EF Core introduced the new ThenInclude() extension method to load multiple levels of related entities.
As EF Core won't load related properties automatically, so you'll need to explicitly do this, but something like the following should do the trick:
var result = context.Begrip
.Include(x => x.Categories)
.ThenInclude(x => x.category);
Note, intellisense doesn't always work on .ThenInclude at the moment, but the code should still compile even if it gets a red underline.
If you're returning this to the view or an API, you'll likely want to map it to a DTO so you don't have to deal with .Categories[0].category.Name etc.

Related

EF core many to many with joining table Get data

I have a many-to-many relationship in EF core. I would like to retrieve all the users and roles assigned to them where UserRoles is active (IsActive)
I have three tables User, Role, UserRoles (see below).
According to business requirements, the user can exist without a role. Moreover, the role can exist without a user.
IsActive field in the RoleUsers table determines role is assigned to the user or not. IsActive is also used for soft delete.
Below is my linq query
**return await _context.User
.Include( c=> c.Role)
.ThenInclude(e=> e.UserRoles.Where(u=> u.IsActive)))
.ToListAsync<User>();**
SQL Server query would be something like below
public class User
{
public string UserId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public ICollection<Role> Roles { get; set; }
public List<UserRoles > UserRoles { get; set; }
}
public class Role
{
public Guid RoleId{ get; set; }
public string Description{ get; set; }
public ICollection<User> Users { get; set; }
public List<UserRoles > UserRoles { get; set; }
}
public class UserRoles
{
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public int Id { get; set; }
public string UserId { get; set; }
public Guid RoleId{ get; set; }
public User User { get; set; }
public Role Role{ get; set; }
public bool IsActive{ get; set; }
}
public DbSet<Role> Role{ get; set; }
public DbSet<User> User { get; set; }
public DbSet<UserRole> UserRole { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>()
.HasMany(m => m.Roles)
.WithMany(g => g.Users)
.UsingEntity<UserRoles>(
mg => mg.HasOne(prop => prop.Role)
.WithMany(p => p.UserRoles)
.HasForeignKey(prop => prop.RoleId),
mg => mg.HasOne(prop => prop.User)
.WithMany(p => p.UserRoles)
.HasForeignKey(prop => prop.UserId),
mg =>
{
mg.HasKey(prop => new { prop.RoleId, prop.UserId });
mg.Property(prop => prop.CreatedOn).HasDefaultValueSql("GETUTCDATE()");
}
);
base.OnModelCreating(modelBuilder);
}

Recommended approach to show a custom user property in a view?

I'm trying to build a simple helpdesk application. In this app, when a ticket is created and displayed, I want to show the first name of the creating user. I am trying to solve how to do this in the best possible way.
I've extended the ApplicationUser class and added FirstName and LastName columns. I also created two foreign keys in my Tickets table, one for the user who created the ticket and one for the agent gets assigned to that ticket. So when the ticket is displayed, I need to show both creators and agents first name + last name's, instead of their UserId's.
This is my ApplicationUser class
public class ApplicationUser : IdentityUser
{
public string FirstName { get; set; }
public string LastName { get; set; }
public ICollection<Ticket> Users { get; set; }
public ICollection<Ticket> Agents { get; set; }
}
This is my model:
public class Ticket
{
public int Id { get; set; }
public string Subject { get; set; }
public string Body { get; set; }
public int Status { get; set; }
public string UserId { get; set; }
public string AgentId { get; set; }
public DateTime Created { get; set; }
public DateTime LastUpdated { get; set; }
public DateTime? Completed { get; set; }
public bool Muted { get; set; }
[ForeignKey("UserId")]
public virtual ApplicationUser TicketUser { get; set; }
[ForeignKey("AgentId")]
public virtual ApplicationUser TicketAgent { get; set; }
}
This is my DbContext:
public DbSet Tickets { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity()
.HasOne(m => m.TicketUser)
.WithMany(t => t.Users)
.HasForeignKey(m => m.UserId)
.OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity()
.HasOne(m => m.TicketAgent)
.WithMany(t => t.Agents)
.HasForeignKey(m => m.AgentId)
.OnDelete(DeleteBehavior.Restrict);
}
This is the controller action to display a specific ticket:
[HttpGet]
public ViewResult Tickets(int id)
{
TicketDetailsViewModel ticketDetailsViewModel = new TicketDetailsViewModel()
{
Ticket = _ticketRepo.GetTicket(id)
};
return View(ticketDetailsViewModel);
}
This is my viewmodel:
public class TicketDetailsViewModel
{
public Ticket Ticket { get; set; }
}
Now, I can display the full name in my view if I do this:
#inject UserManager userManager;
#{
var ticketUser = (await userManager.FindByIdAsync(Model.Ticket.UserId)).FirstName + " " + (await userManager.FindByIdAsync(Model.Ticket.UserId)).LastName;
}
But I am not sure if this is a good way to do it. I'd like to learn what is the best way to achive this.
Thank you very much.
You can define a _fullname in your ApplicationUser , and then if firstname and lastname both exist, you can directly call Fullname, like:
public class ApplicationUser : IdentityUser
{
private string _fullName; //new property
public string FirstName { get; set; }
public string LastName { get; set; }
[NotMapped]
public string FullName
{
get
{
return _fullName = this.FirstName + "." + this.LastName;
}
set
{
_fullName = value;
}
}
public ICollection<Ticket> Users { get; set; }
public ICollection<Ticket> Agents { get; set; }
}
In view, just call FullName:
#{
var ticketUser = (await userManager.FindByIdAsync(Model.Ticket.UserId)).FullName;
}
In these scenarios I usually prefer to go with an extension method instead of an additional property like proposed by user Jerry Cai, the model remains lighter and cleaner imho:
public static class ApplicationUsersExtensions
{
public static string GetFullname(this ApplicationUser user)
{
return $"{user.FirstName}.{user.LastName}";
}
}

Is it possible to link one table to another with entity framework core without FKs?

I have 2 tables company and user. Company will have one created by user and one modified user - these will be admin users. User will belong to one company but one admin user could create or modify multiple companies.
I'm having a hard time using entity framework core in my .net core app to join company and user so when I get a company record I have the created by and modified user information.
My company and user classes look like this:
public class Company
{
[Key]
public Guid Id { get; set; }
public DateTime Created { get; set; }
public Guid Created_By { get; set; }
public virtual ApplicationUser CreatedByUser { get; set; }
public DateTime Modified { get; set; }
public Guid Modified_By { get; set; }
public virtual ApplicationUser ModifiedByUser { get; set; }
public string Company_Name { get; set; }
}
public class ApplicationUser: IdentityUser<Guid>
{
[Column("ID")]
public override Guid Id { get; set; }
[Column("CREATED")]
public DateTime Created { get; set; }
[Column("CREATED_BY")]
public Guid? CreatedBy { get; set; }
[Column("MODIFIED")]
public DateTime Modified { get; set; }
[Column("MODIFIED_BY")]
public Guid? ModifiedBy { get; set; }
[Column("FIRST_NAME")]
public string FirstName { get; set; }
[Column("LAST_NAME")]
public string LastName { get; set; }
[Column("EMAIL")]
public override string Email { get; set; }
[Column("NORMALIZED_EMAIL")]
public override string NormalizedEmail { get; set; }
[Column("EMAIL_CONFIRMED")]
public override bool EmailConfirmed { get; set; }
[Column("USER_NAME")]
public override string UserName { get; set; }
[Column("NORMALIZED_USER_NAME")]
public override string NormalizedUserName { get; set; }
[Column("COMPANY_ID")]
public Guid CompanyId { get; set; }
[Column("PHONE_NUMBER")]
public override string PhoneNumber { get; set; }
[Column("PHONE_NUMBER_CONFIRMED")]
public override bool PhoneNumberConfirmed { get; set; }
[Column("TITLE")]
public string Title { get; set; }
[Column("ACTIVE")]
public bool Active { get; set; }
[Column("ROLE_ID")]
public int UserRoleId { get; set; }
[Column("TYPE_ID")]
public int TypeId { get; set; }
[Column("PASSWORD_HASH")]
public override string PasswordHash { get; set; }
[Column("SECURITY_STAMP")]
public override string SecurityStamp { get; set; }
[Column("CONCURRENCY_STAMP")]
public override string ConcurrencyStamp { get; set; }
[Column("TWO_FACTOR_ENABLED")]
public override bool TwoFactorEnabled { get; set; }
[Column("LOCKOUT_END")]
public override DateTimeOffset? LockoutEnd { get; set; }
[Column("LOCKOUT_ENABLED")]
public override bool LockoutEnabled { get; set; }
[Column("ACCESS_FAILED_COUNT")]
public override int AccessFailedCount { get; set; }
}
My DbContext class looks like this:
public class DbContext : IdentityDbContext<ApplicationUser, ApplicationRole, Guid, ApplicationUserClaim, ApplicationUserRole, IdentityUserLogin<Guid>, IdentityRoleClaim<Guid>, IdentityUserToken<Guid>>
{
public DbContext(DbContextOptions<DbContext> options) : base(options)
{
}
public virtual DbSet<Company> Companies { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<Company>().ToTable("COMPANY").Property<Guid>("Created_By");
builder.Entity<Company>().HasOne(x => x.CreatedByUser).WithOne().HasForeignKey("Created_By");
builder.Entity<ApplicationUser>().ToTable("USER");
builder.Entity<ApplicationUser>().HasKey(x => x.Id);
builder.Entity<ApplicationUserClaim>().ToTable("USER_CLAIMS");
builder.Entity<ApplicationRole>().ToTable("IDENTITY_ROLES");
builder.Entity<IdentityUserRole<Guid>>().HasKey(p => new { p.UserId, p.RoleId });
builder.Entity<ApplicationUserRole>().ToTable("IDENTITY_USER_ROLES");
}
}
And I was trying to get companies like this:
public async Task<List<Company>> GetAllCompanies()
{
return await _locationDbContext.Companies.ToListAsync();
}
Currently I am getting this error:
System.InvalidOperationException: 'You are configuring a relationship between 'Company' and 'ApplicationUser' but have specified a foreign key on 'Created_By'. The foreign key must be defined on a type that is part of the relationship.'
Is there an easier way to do this? Really all I want is the username of the user that created of modified the company record? If I was doing this with just sql I would just use a basic Join but Im not sure how to do that with entity framework. Worst case I would just get all the companies and then loop through doing a select on the user table where ID = Created_By
As #IvanStoev noticed without FKs it is not possible. But you can still use EF to join 2 tables.
In your case you have to unmap user from company and maybe it is a good idea to make user Guid nullable:
public class Company
{
[Key]
public Guid Id { get; set; }
public string Company_Name { get; set; }
public DateTime Created { get; set; }
public DateTime Modified { get; set; }
public Guid Created_By { get; set; }
public Guid Modified_By { get; set; }
[NotMapped]
public ApplicationUser CreatedByUser { get; set; }
// or better
[NotMapped]
public string CreatedByUser { get; set; }
[NotMapped]
public ApplicationUser ModifiedByUser { get; set; }
//or better
[NotMapped]
public string ModifiedByUser { get; set; }
}
and remove
builder.Entity<Company>().HasOne(x => x.CreatedByUser).WithOne().HasForeignKey("Created_By");
you still can join them like this
var companies= (
from c in _locationDbContext.Companies
join uc in _locationDbContext.ApplicatonUser on c.Created_By equals uc.Id
join um in _locationDbContext.ApplicatonUser on c.Modified_By equals um.Id
select new Company
{
....
CreatedByUser = uc,
ModifiedByUser = um
// or usually
CreatedByUser = uc.FirstName + " " + uc.LastName,
ModifiedByUser = um.FirstName + " " + um.LastName,
}).ToList();
This is one way of accomplishing this.
This is a simplified version of class Company:
public class Company
{
public int CompanyID { get; set; }
public string Name { get; set; }
//
// Relations
public string CreatorID { get; set; }
public ApplicationUser Creator { get; set; }
public string LastModifiedByID { get; set; }
public ApplicationUser LastModifiedBy { get; set; }
}
CreatorID and LastModifiedByID will be used by EF for determining the FK's. These are the fields you have to work with when updating the Db, not Creator and LastModfiedBy, yet you can also do it but with more lines of code.
Now, you can add companies the usual way, this code searches for the company, and creates one if not found, just sample code, not serious, really:
var companyName = "My company";
var company = context.Companies.Include(c => c.Creator).Include(c => c.Creator).FirstOrDefault(c => c.Name == companyName);
if (company == null)
{
company = new Company
{
Name = "My company",
CreatorID = user.Id,
LastModifiedByID = user.Id
};
context.Companies.Add(company);
context.SaveChanges();
}
And retrieving the companies with all relations filled is a matter or using calls to Include() and ThenInclude(), like here:
var companies = context.Companies.Include(c => c.Creator).Include(c => c.Creator).ToList();
I omitted all filtering logic for the sake of simplicity.

Entity Framework Core one to many bidirectional

Coming from the world of mighty nHibernate trying to get bidirectional relation to work in Entity Framework Core.
Here are my simplified entities:
public class User
{
public int id { get; set; }
public string username { get; set; }
public ICollection<Login> Logins { get; set; }
}
public class Login
{
public int id { get; set; }
public string email { get; set; }
public User User { get; set; }
}
Here is my mapping:
public DbSet<User> Users { get; set; }
public DbSet<Login> Logins { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>()
.ToTable("users")
.HasMany(x=>x.Logins)
.WithOne(x=>x.User)
.HasForeignKey(x=>x.user_id);
modelBuilder.Entity<Login>()
.ToTable("logins")
.HasOne(d=>d.User)
.WithMany(d=>d.Logins)
.HasForeignKey(e => e.user_id);
}
When I query User, Logins collection is fetched correctly. However when I query Login the User comes null.
var login = DBContext.Logins.Where(x => x.email == email).SingleOrDefault();
// login.User is null

Linq with EF, include specific columns

I have two classes:
public class Category
{
public int Id { get; set; }
[Required]
[MaxLength(255)]
public string Name { get; set; }
public int? CategoryId { get; set; }
public double Weight { get; set; }
public ICollection<Article> Articles { get; set; }
public bool Hidden { get; set; }
}
public class Article
{
public int Id { get; set; }
[StringLength(255)]
public string Title { get; set; }
public string Body { get; set; }
public double Weight { get; set; }
public Category Category { get; set; }
public int? CategoryId { get; set; }
}
I would like to select some Categories including Articles, but without Article.Body. Method syntax is more preferred.
Something like:
IEnumerable<Category> categories = _context
.Categories
.Where(c => c.Hidden == false)
.Include(c => c.Articles)
.OrderBy(c => c.Weight);
Not sure how to specify which columns exactly to select (eagerly) on the included Articles.
Include doesn't allow projections, you can only include complete entities.
But there is a way out.
This is a typical case that you should solve by table splitting. By table splitting you "split" a table over two (or more) entities, so it's easier to filter e.g. light data from heavy data or public data from secure data.
In your case the class model (for Article) would look like this:
public class Article
{
public int Id { get; set; }
[StringLength(255)]
public string Title { get; set; }
public double Weight { get; set; }
public Category Category { get; set; }
public int? CategoryId { get; set; }
public virtual ArticleBody ArticleBody { get; set; }
}
public class ArticleBody
{
public int Id { get; set; }
public string Text { get; set; }
}
And the mappings:
modelBuilder.Entity<Article>()
.HasRequired(a => a.ArticleBody)
.WithRequiredPrincipal();
modelBuilder.Entity<Client>().ToTable("Article");
modelBuilder.Entity<ArticleBody>().ToTable("Article");
Now if you do...
_context.Categories
.Where(c => !c.Hidden)
.Include(c => c.Articles)
...you'll see that only Articles without body texts will be selected in the generated SQL.
If you want the body as well, you do
_context.Categories
.Where(c => !c.Hidden)
.Include(c => c.Articles.Select(a => a.ArticleBody))
Sorry if i did not understand your question, but I think you can specify what columns you want in your select statement.
Simple example:
var query = from c in Categories
select c.Name, c.CategoryId;