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

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.

Related

Why Entity Framework Core 7.0.2 for SQLite is returning nulls even tho the data exists?

I have been having a hard time trying to figure out why EF6 is not finding a record when I use the Id (which is currently a GUID) even tho the record exists.
The required field is as a String inside the SQLite database and in the model is a GUID. Let me share part of my code to make things more clear:
User Model:
public class User
{
public Guid Id { get; set; } //Todo: Filter users by the Id
public string Password { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
public DateTime CreationDate { get; set; } = DateTime.UtcNow;
public int AccountActive { get; set; } = 1;
public Role Role { get; set; } = new Role();
}
The method inside the repo that I'm having problems with:
public User GetUser(string Id)
{
if(Guid.TryParse(Id, out Guid result))
{
var user = _context.Users.Include(x => x.Role).SingleOrDefault(u => u.Id == result);
if(user != null)
return user;
}
throw new Exception("User not found!");
}
To me, it looks like EFCore is not able to parse the GUID back to a string to do the correct query. Trying out to see what is going on on the query side I was trying to output the query to the Debug by adding a new string to the appsettings.Development.json like this:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"Microsoft.EntityFrameworkCore.Database.Command": "Information"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"Default": "Data Source=Database/PTasks.db"
}
}
The Output is showing much more information but no queries are been shown even after adding the option for sensitive data logging with optionsBuilder.UseSqlite(connectionString).EnableSensitiveDataLogging(true)
So far I have no clues on what is going on since this query has always worked for me over SQL Server, so I tend to think that this might be an issue with the use of EF6 and SQLite. As you can see I have been trying to inspect the Query but with no luck, so right now I'm out of ideas.
When I debug the method the var user = _context.Users.Include(x => x.Role).SingleOrDefault(u => u.Id == result); ends up as null even if the Id param was correctly parsed as a Guid and the Id exists in the database.
Just to add some extra information, I just changed the method to use RawSql and it is returning the expected data, but I really want to understand what is going on.
This is the new version:
public User GetUser(string Id)
{
if(Guid.TryParse(Id, out Guid result))
{
//var user = _context.Users.Include(x => x.Role).SingleOrDefault(u => u.Id == result);
var user = _context
.Users
.FromSqlRaw($"select * from Users where Id = '{Id}'")
.SingleOrDefault();
if(user != null)
return user;
}
throw new Exception("User not found!");
}
Of course, it is missing the join to the Roles table but that is not relevant to the issue.
So... I just found out this is indeed a Bug and it's been around since 2018. the issue arises when you have a PK in a table and that PK is a GUID when you try to filter by the PK, EF does not convert from Guid to String, so basically, the query will be wrong.
The workaround is simple but not very "elegant":
public User GetUser(string Id)
{
if(Guid.TryParse(Id, out Guid result))
{
var user = _context
.Users
.Include(x => x.Role)
.Single(u => u.Id.ToString().Equals(result.ToString()));
if (user != null)
return user;
}
throw new Exception("User not found!");
}
It works... but I hate this ToString() solution!

EF Core. Get data from many-many relationship

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

Can't get an entity property

There is a problem with my Db which I figured only now, when I started to work at the web api. My USER entity:
public class User { get; set; }
{
public int UserId { get; set; }
public string Name { get; set; }
}
And this is ACTIVITY
public class Activity
{
public int ActivityId { get; set; }
public User User { get; set; }
}
I added an activity and checked in SSMS. Everything seems to be good, there is a field named UserId which stores the id. My problem is when I try to get a User from an Activity because I keep getting null objects. I didn't set anything special in my DbContext for this.
This is where I'm trying to get an User from an Activity object:
public ActionResult ActivityAuthor(int activityId)
{
Activity activityItem = unitOfWork.Activity.Get(activityId);
return Json(unitOfWork.User.Get(activityItem.User.UserId));
}
Relation between User and Activity
The User property of Activity class should be marked as virtual. It enables entity framework to make a proxy around the property and loads it more efficiently.
Somewhere in your code you should have a similar loading method as following :
using (var context = new MyDbContext())
{
var activity = context.Activities
.Where(a => a.ActivityId == id)
.FirstOrDefault<Activity>();
context.Entry(activity).Reference(a => a.User).Load(); // loads User
}
This should load the User object and you won't have it null in your code.
Check this link for more information msdn
my psychic debugging powers are telling me that you're querying the Activity table without Include-ing the User
using System.Data.Entity;
...
var activities = context.Activities
.Include(x => x.User)
.ToList();
Alternatively, you don't need Include if you select properties of User as part of your query
var vms = context.Activities
.Select(x => new ActivityVM() {UserName = x.User.Name})
.ToList();

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);
}

Disable username validation on edit

I created my validation for unique username and now I can't edit my user. It's saying that username is already taken which makes sense since it's taken by the user that I'm trying to edit. I don't even want to edit username but because of this error I cannot edit any other field as well.
How can I disable unique username validation for my EDIT action?
Validator
public override bool IsValid(object value)
{
if (value == null) return false;
FinanceDataContext _db = new FinanceDataContext();
var user = _db.Users.ToList().Where(x => x.Username.ToLower() == value.ToString().ToLower()).SingleOrDefault();
if (user == null) return true;
return false;
}
Action
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(User u)
{
// Get user we want to edit
var user = _db.Users.Where(x => x.ID == u.ID).SingleOrDefault();
if (user == null) return HttpNotFound();
// Set values and save changes
user.Address = u.Address;
if (!string.IsNullOrEmpty(u.Password)) user.Password = Infrastructure.Encryption.SHA256(u.Password);
_db.SaveChanges(); // validation error
return null;
}
Model
public class User
{
public int ID { get; set; }
[Required]
[UniqueUsername(ErrorMessage = "Username is already taken")]
public string Username { get; set; }
[Required]
public string Password { get; set; }
public string Address { get; set; }
}
Error
Validation failed for one or more entities. See
'EntityValidationErrors' property for more details.
Pass the "ID" property in "AdditionalFields" parameter of UniqueUsername Attribute so I guess your code should be like this
Class Property :
[Required]
[UniqueUsername(ErrorMessage = "Username is already taken", AdditionalFields = "ID")]
public string Username { get; set; }
Validate Action :
public ActionResult UniqueUsername(string userName, int id)
{
FinanceDataContext _db = new FinanceDataContext();
var user = _db.Users.ToList().SingleOrDefault(x => x.Username.ToLower() == value.ToString().ToLower() && x.ID != id);
return Json(user == null, JsonRequestBehavior.AllowGet);
}
Hope this will help !!
Could you pass in the user id into the IsValid method and make sure the user returned doesn't have the same user id?
One thing I would like to contribute.
This code:
var user = _db.Users.ToList().Where(x => x.Username.ToLower() == value.ToString().ToLower()).SingleOrDefault();
Is not performatic, because when you do a ToList() you execute the whole query. In this case, what you are doing is retrieving ALL USERS from the database and them doing your filter in memory.
I would sugest you to do this:
_db.Users.Where(x => x.Username.ToLower() == value.ToString().ToLower()).SingleOrDefault();
Since you just want to retrieve one record, there is no need to call the ToList() method. Just the SingleOrDefault() at the end is enough.
Fixed it by passing whole object to my validator and then checking id's as well as usernames.
public override bool IsValid(object value)
{
var user = (User)value;
if (user == null) return true;
FinanceDataContext _db = new FinanceDataContext();
var u = _db.Users.Where(x => x.Username.ToLower() == user.Username.ToLower() && x.ID != user.ID).SingleOrDefault();
if (u == null) return true;
return false;
}

Categories