EF Core. Get data from many-many relationship - c#

I'm new to EF Core. I have many-many relationship between 2 tables. In total, I have these 3 tables:
Tenant.
UserTenant.
User.
User has property "email". I want to get all tenants that are related to user by given user email. How to do that?
I would do something like this but I think is bad approach.
var user = await dbContext.Users.FirstAsync(u => u.Email == userEmail);
var userTenants = dbContext.UserTenants.Where(u => u.UserId == user.Id);
etc...

What you could do is to create a Class ex:
public class UserEmailViewModel
{
public int Id { get; set; }
public string Email{ get; set; }
}
and Then
var userEmail = await dbContext.SqlQuery<UserEmailViewModel>("select Id, Email from User U , Tenant T , UserTenant UT where u.id=ut.Userid and t.Id=UT.TenantId ")
Note That you should add Properties to UserEmailViewModel if you want to add column to the query

Related

Querying EF Core identity roles

I'm using .NET Core 3.1 and I want to query the users in my database.
So I modified my ApplicationUser class as follows:
public class ApplicationUser : IdentityUser
{
public ICollection<IdentityUserRole<string>> UserRoles { get; set; }
}
And now I'm trying to query the Users.
var users = from u in DbContext.Users
select new UserViewModel
{
Email = u.Email,
EmailConfirmed = u.EmailConfirmed,
Phone = u.PhoneNumber,
Roles = u.UserRoles.Select(r => r.Role.Name) // Whoops! Won't work
};
The problem is that UserRoles have a RoleId property, but they don't have a Role property.
So how would I get the name of those roles?
Update:
Based on Jawad's comments, I changed my code as follows:
public class ApplicationUser : IdentityUser
{
public ICollection<IdentityRole> Roles { get; set; }
}
And
Users = (from u in DbContext.Users
select new UserViewModel
{
Email = u.Email,
EmailConfirmed = u.EmailConfirmed,
Phone = u.PhoneNumber,
Roles = u.Roles.Select(r => r.Name)
})
.ToList();
I was thinking I couldn't do that because it's a many-to-many relationship. But in fact, it does compile. However, when I run it I get an error.
Invalid column name 'ApplicationUserId'.
I'm not sure where that column name is being referenced. It doesn't appear to be a column name in the database.
Update 2:
Turns out, the error above is because a database migration was pending. However, I didn't want to change the database. As I initially suspected, I can't do ApplicationUser.ICollection<IdentityRole> because there is an intermediate table.
So I got this to run, but it returned no results (because there are no foreign keys from IdentityRole to ApplicationUser.
So, the question stands: how can I query users along with their roles?
So, I don't know why IdentityUserRole doesn't include an IdentityRole property. This seems to be an unnecessary limitation.
In the end, I just changed my query.
var users = await (from u in DbContext.Users
join ur in DbContext.UserRoles on u.Id equals ur.UserId
join r in DbContext.Roles on ur.RoleId equals r.Id
select new
{
u.Email,
u.EmailConfirmed,
u.PhoneNumber,
Role = r.Name
})
.ToListAsync();
Users = (from u in users
group u.Role by u into g
select new UserViewModel
{
Email = g.Key.Email,
EmailConfirmed = g.Key.EmailConfirmed,
Phone = g.Key.PhoneNumber,
Roles = string.Join(", ", g)
})
.ToList();
I couldn't get the group by to work within the query so I'm group it after the data has been retrieved.
Anyway, it works.
You should create a new class called something like ApplicationUserRole that inherits from IdentityUserRole<string> then you just need to define the relationships between ApplicationUser -> ApplicationUserRole and ApplicationRole -> ApplicationUserRole.
modelBuilder.Entity<ApplicationUserRole>(entity =>
{
entity
.HasOne(x => x.Role)
.WithMany(x => x.UserRoles)
.HasForeignKey(x => x.RoleId);
entity
.HasOne(x => x.User)
.WithMany(x => x.UserRoles)
.HasForeignKey(x => x.UserId);
});
You should also replace any references to the Identity classes with your Application classes.
public class ApplicationUserRole : IdentityUserRole<string>
{
public ApplicationUser User { get; set; }
public ApplicationRole Role { get; set; }
}
public class ApplicationUser : IdentityUser<string>
{
public ICollection<ApplicationUserRole> UserRoles { get; set; }
}
public class ApplicationRole : IdentityRole<string>
{
public ICollection<ApplicationUserRole> UserRoles { get; set; }
}

Why is EF core one-to-one relationship not working as expected?

I have a User and a RefreshToken. I need to create a one-to-one relationship between those two objects. My RefreshToken object looks like this
`public class RefreshToken
{
[ForeignKey("User")]
public string RefreshTokenID { get; set; }
public string UserId { get; set; }
public User User { get; set; }
public string Token { get; set; }
}`
My User object looks like this
`public class User : IdentityUser
{
public RefreshToken RefreshToken { get; set; }
}`
Here is how I am persisting the RefreshToken for a user using _refreshTokenRepository:
`public async Task<bool> SaveAsync(User user, string newRefreshToken, CancellationToken ct = default(CancellationToken))
{
if(user.RefreshToken != null) return false;
RefreshToken rt = new RefreshToken(newRefreshToken, DateTime.Now.AddDays(5), user.Id);
await _dbContext.RefreshTokens.AddAsync(rt);
await _dbContext.SaveChangesAsync(ct);
return true;
}`
The issue happens when, user is redirected to the '/refresh' route, and when I need to check the User's refresh token against the one coming back from the client. This property check is always null:
`var user = _userManager.Users.SingleOrDefault(u => u.UserName == username);
if(user == null || user.RefreshToken.Token != refreshToken) return BadRequest();`
The user is in fact the right user and is found, but the RefreshToken property is null.
I have tried using fluent API to create this relationship. Same outcome. The refresh tokens are successfully persisted to the database with the right user ID in the table, but the navigational property is not working. Any ideas why?
You should modify this line:
var user = _userManager.Users.SingleOrDefault(u => u.UserName == username);
By adding .Include(u => u.RefreshToken), So, the line should look like this:
var user = _userManager.Users.Include(u => u.RefreshToken).SingleOrDefault(u => u.UserName == username);
This will tell the store to also load the related RefreshToken entities.

Define a LAMBDA query in a property so it can be reused

I'm trying to defined a lambda query in a property of my code first EF model as seen below as, GetLatestTransaction :
public class ApplicationUser : IdentityUser
{
public string FirstName { get; set; }
public string LastName { get; set; }
public virtual List<TransactionModel> Transactions { get; set; }
public TransactionModel GetLatestTransaction {
get {
return Transactions.OrderByDescending(x => x.Created).FirstOrDefault();
}
}
}
The reason for this is that I don't want to have to retype this query in many places and by having it in one place reduce the chances of a bug.
I want to use this in a query like this:
var user = _DB.Users
.Select(u => new UserDetailsView()
{
Id = u.Id,
FirstName= u.FirstName,
LastName= u.LastName,
Balance = u.GetLatestTransaction.ValueResult
}).FirstOrDefault(x => x.Id == userId);
This is however resulting in this error:
System.NotSupportedException: 'The specified type member 'GetLatestTransaction' is not supported in LINQ to Entities. Only initializers, entity members, and entity navigation properties are supported.'
Is there some way to achieve this without storing another relation to the latest transaction on the user and having to update it every time there is a new transaction?
Edit: I would also like to do it as above to avoid making another query to the database, I want it all in one go to improve performance.
Your ApplicationUser class represents the table in the database. It does not represent the usage of the data in the table.
Quite a lot of people think it is good practice to separate the database structure from the usage of the data. This separation is quite often done using the repository pattern. The repository is an abstraction from the internal datastructure of the database. It allows you to add functionality to your classes without demanding this functionality in the control classes that communicate with the database.
There are numerous articles about the repository. This one helped me to understand what functionality I should put in my entity framework classes and which in the repository.
So you'll need a class that represents the elements in your database table and one that represents the applicationUsers with only their LatestTransaction
The class that represents the database table:
class ApplicationUser : IdentityUser
{
public int Id {get; set;}
public string FirstName { get; set; }
public string LastName { get; set; }
public virtual List<TransactionModel> Transactions { get; set; }
}
ApplicationUser with the latest transaction
class AppicationUserExt : <base class needed?>
{
public int Id {get; set;}
public string FirstName { get; set; }
public string LastName { get; set; }
public TransactionModel LatestTransaction { get; set; }
}
The function to get your extended ApplicationUser is an extension function of your ApplicationUser. Input: IQueryable<ApplicationUser output: IQueryable<ApplicationUserExt>
static class MyDbContextExtensions
{
// returns ne ApplicationUserExt for every ApplicationUser
public IQueryable<ApplicationUserExt> ToExtendedUsers(this IQueryable<ApplicationUser> applicationUsers)
{
return applicationUsers
.Select(user => new ApplicationUserExt()
{
Id = user.Id,
FirstName = user.FirstName,
LastName = user.LastName,
LatestTransaction = user.Trnasactions
.OrderByDescenting(transaction => transaction.CreationDate)
.FirstOrDefault(),
}
}
}
}
So whenever you have a query with the ApplicationUsers you want, you can use ToExtendedUsers() to get the extended suers
using (var dbContext = new MyDbContext(...))
{
// you wanted to have a query like:
var result dbContext.ApplicationUsers
.Where(user => user.FirstName = "John"
&& user.LastName = "Doe");
// you'll have to add ToExtendedUsers:
var result = dbContext.ApplicationUsers
.Where(user => user.FirstName = "John"
&& user.LastName = "Doe");
.ToExtendedUsers();
}
As the result is still an IQueryable, no query has been done yet. You can still add LINQ statements before the query is done:
var result2 = result
.Where(user.LatestTransaction.Year == 2018)
.GroupBy(user => user.LatestTransaction.Date)
.OrderBy(group => group.Key)
.Take(10)
.ToList();
You see, that you can still do all kinds of LINQ stuff as long as it is an ApplicationUser. As soon as you need the LatestTransaction you convert it to an ApplicationUserExt and continue concatenating your linq statements.

How to authorize a user to only see his own records with asp.net Identity 2.0

There must be an easy solution for such a generic question, so I apologize upfront for my ignorance:
I have a multi-user Web-app (Asp.net MVC5 with EF6) that a.o. allows users to view and/or modify their relevant data stored in several related tables (Company, Csearch, Candidate). (for more details see below). They should NOT see any other data (e.g. by tampering with the URL).
I use Asp.net Identity 2.0 for authentication and would like to use it for the mentioned authorization as well. Userdata is stored in the standard AspNetUser Table. I use only one context for both Identity and my Business Tables.
I guess I have to either use Roles or maybe Claims to solve this, but I cannot find any guidance on how to do that. Can anyone point me in the right direction?
I have currently solved it (for the Company Model) by adding a LINQ condition to the CompanyController, but this does not appear to be a very secure and proper way of solving the problem.
public ActionResult Index(int? id, int? csearchid)
{
var companies = db.Companies
.OrderBy(i => i.CompanyName)
.Where(t => t.UserName == User.Identity.Name);
return View(companies);
My DataModel is straightforward and I had it scaffolded using Visual Studio 2017
Through EF6 Code first I have constructed a Relational Datamodel which is roughly as follows:
a COMPANY can have multiple SEARCHES (one to many).
Each Search can have multiple CANDIDATES (one to many).
A COMPANY can have multiple USERS logging in.
Users are save in the AspNetUsers table genberated by ASP.Net Identity.
My Company model looks as follows:
public class Company
{
public int CompanyID { get; set; }
// Link naar de Userid in Identity: AspNetUsers.Id
[Display(Name = "Username")]
public string UserName { get; set; }
public string CompanyName { get; set;}
public string CompanyContactName { get; set; }
[DataType(DataType.EmailAddress)]
public string CompanyEmail { get; set; }
public string CompanyPhone { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; }
//One to Many Navigatie links
public virtual ICollection<Csearch> Csearches { get; set; }
Once the user is identified, you can make sure the user can only access its own data. You cannot use roles for that, since that will only define the level of access. But you can use claims.
Out-of-the-box there is a seperation of concerns. Maintain this seperation. You are not meant to query the Identity tables directly. Use the userManager for that. Also never use an Identity object as ViewModel. You may expose more than you mean to. If you keep this seperation, you'll see that it is in fact much easier.
The identity context contains all data to identify the user, the business context contains all business information, including user information. You may think that this is redundant, but the login user has really nothing in common with the business user. The login emailaddress may differ from the business.user.emailaddress (what is the meaning of the emailaddress in both cases?). Also consider the possibility to have users that cannot login (anymore).
As a rule of thumb always consider if the information is part of the identity or part of the business.
When do you need the ApplicationUser? Only for the current user or when managing users. When you query users, always use the business.user. Because all the information you need should be available there.
For the current user, add claims with the information you need. The advantage of claims is that you won't have to query the database on each call to retrieve this information, like the corresponding UserId and the (display)UserName.
How to add claims
You can, without having to extend the ApplicationUser class, add a claim to the user by adding a row to the AspNetUserClaims table. Something like:
userManager.AddClaim(id, new Claim("UserId", UserId));
On login the claim will be automatically added to the ClaimsIdentity.
You can also add claims for properties that extend the ApplicationUser:
public class ApplicationUser : IdentityUser
{
public int UserId { get; set; }
public string DisplayUserName { get; set; }
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
userIdentity.AddClaim(new Claim("UserId", UserId));
userIdentity.AddClaim(new Claim("DisplayUserName", DisplayUserName));
return userIdentity;
}
}
How to read claims
In the controller you can read the claim with code like this:
var user = (System.Security.Claims.ClaimsIdentity)User.Identity;
var userId = user.FindFirstValue("UserId");
You can use userId in your queries to filter the data for the current user or even use business.users as the only entry to retrieve data. Like db.Users(u => u.Id == userId).Companies.ToList();
Please note, the code is just an example. I didn't test all of it. It is just to give you an idea. In case something isn't clear, please let me know.
It's pretty simple really. To illustrate with the example Company you provided. Note that you should use UserId to join rather than UserName since UserName can change, but UserId will always be unique.)
Instead of having UserName in your Company table, you need to change that to UserId. Then you join the AspNetUsers table with your Company table on UserId.
For example (I prefer to use the query syntax rather than the fluent syntax):
var companies = from c in db.Companies join u in db.AspNetUsers
on c.UserId equals u.UserId
orderby c.CompanyName
where u.UserName = User.Identity.Name
select c;
If you need the username as well, then include that in your select
select new { Company = c, User = u.UserName };
However, this model does not work if you want to have multiple users per company. You either need to add CompanyId to the users table (assuming a user can't be a member of more than one company) or create a many-to-many join if a user can be a member of multiple companies.
So rather than linking the user to the company, you link the company to the user. Your current model only allows one user per company.
Another thing I see wrong here is the use of DisplayName in your entity object. That seems to indicate you are using the entity in your MVC view, which you shouldn't do. You should create a separate ViewModel.
Here is how it should look like for multiple users per company:
public class Company
{
public int CompanyID { get; set; }
// Link naar de Userid in Identity: AspNetUsers.Id
// [Display(Name = "Username")] <-- Get rid of these
// public string UserName { get; set; } <-- get rid of these
...
}
public class ApplicationUser : IdentityUser
{
public int CompanyId { get; set; }
}
Then change your query to:
var companies = from c in db.Companies join u in db.AspNetUsers
on c.CompanyId equals u.CompanyId // <-- Change to this
orderby c.CompanyName
where u.UserName = User.Identity.Name
select c;
I made it in the following way:
I added UserId property to the Company class. (It is string type because at SQL it is NVARCHAR type)
public class Company
{
public string UserId { get; set; }
public int CompanyID { get; set; }
// Link naar de Userid in Identity: AspNetUsers.Id
[Display(Name = "Username")]
public string UserName { get; set; }
public string CompanyName { get; set;}
public string CompanyContactName { get; set; }
[DataType(DataType.EmailAddress)]
public string CompanyEmail { get; set; }
public string CompanyPhone { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; }
//One to Many Navigatie links
public virtual ICollection<Csearch> Csearches { get; set; }
}
In the Create controller for getting current logged in user id I used How to get the current logged in user ID in ASP.NET Core? post. In brief UserId = User.FindFirstValue(ClaimTypes.NameIdentifier)
public class CompanyController : Controller
{
private readonly ApplicationDbContext _context;
private readonly IWebHostEnvironment webHostEnvironment;
public CompanyController (ApplicationDbContext context, IWebHostEnvironment hostEnvironment)
{
_context = context;
webHostEnvironment = hostEnvironment;
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(RecordViewModel model)
{
if (ModelState.IsValid)
{
Company company = new Company
{
UserId = User.FindFirstValue(ClaimTypes.NameIdentifier),
FirstName = model.FirstName,
CompanyName = model.CompanyName,
CompanyContactName = model.CompanyContactName,
CompanyEmail = model.CompanyEmail,
CompanyPhone = model.CompanyPhone
};
_context.Add(company);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
return View(model);
}
}
And for the displaying only records of the current logged in user I use following action:
public async Task<IActionResult> Index()
{
var companyLoggedInUser = from c in _context.Company
where c.UserId ==
User.FindFirstValue(ClaimTypes.NameIdentifier)
select c;
return View(companyLoggedInUser);
}

Entity Framework and LINQ left join and include on same table

I have the following class structure for my Users and the permissions they're in for the different companies they may be associated to:
public class User
{
public Guid Id { get; set; }
public List<Permission> Permissions { get; set; }
public Company DefaultCompany { get; set; }
}
public class Permission
{
public User User { get; set; }
public Company Company { get; set; }
public int PermissionLevel { get; set; }
}
public class Company
{
public Guid Id { get; set; }
public string Name { get; set; }
}
This results in three SQL tables. There is a FK between Permission.User_Id > User.Id and Permission.Company_Id > Company.Id. There is no explicit relationship (ie. FK) between User.DefaultCompany and the Company table. This is on purpose due to a legacy choice in our database schema.
I also have a database repository method that grabs a user by it's Id, and includes the full Company record:
public User GetById(Guid Id)
{
return (from r in this.Context.Users.Include("Permissions.Company")
where r.Id == Id
select r)
.SingleOrDefault();
}
This works fine, but it doesn't set the DefaultCompany property. So I tried setting that by changing this method to the following. It's worth pointing out that the Company record that represents the DefaultCompany shares the same ID value as the User.
public User GetById(Guid Id)
{
return (from r in this.Context.Users.Include("Permissions.Company")
where r.Id == Id
join c in this.Context.Companies on r.Id equals c.Id into companies
from company in companies.DefaultIfEmpty()
select new { User = r, Company = company })
.ToList()
.Select(p => { p.User.DefaultCompany = p.Company; return p.User; })
.SingleOrDefault();
}
And this does, in fact, set the DefaultCompany but it has the side effect of not selecting the Permissions list. I can do this all as two separate operations, as in the following code, but I'd rather not hit the database twice if I don't have to.
public User GetById(Guid Id)
{
var u = (from r in this.Context.Users.Include("Permissions.Company")
where r.Id == Id
select r)
.SingleOrDefault();
u.DefaultCompany = (from r in this.Context.Companies where r.Id == u.Id select r).SingleOrDefault();
return u;
}
Is there another way to accomplish this?
Edit: explaining resulting SQL data model and additional example.
There are two possible solutions for this problem.
The cleanest is to use the Fluent API to indicate the model that there is a 1 to 1 relation between User and Company.
Override the OnModelCreating(DbModelBuilder modelBuilder) method of your context.
Inside it, configure the 1 ro 1 relation like this:
modelBuilder.Entity<User>()
.HasOptional(u => u.DefaultCompany)
.WithRequired();
NOTE: with this configuration there is a relationship of 1 user to 0 or 1 default companies (note the Optional in HasOptional). And the default company must have a User on the other side. When a 1 to 1 (or 1 to 0..1) relation is configured, EF will automatically use the PK of the related tables to create the relation between them. You can fine tune the relation using other Fluent API functions
After doing so, you can include the DefaultCompany using Include():
User user = ctx.Users
.Include(u => u.DefaultCompany)
.SingleOrDefault(u => u.Id == userId);
The other, more ugly solution, is to use your second query and include the missing permissions in the projection, to force EF to recover them from the DB.
// ...
select new { User = r, Company = company, Companies = r.Permissions }
// ...

Categories