null значение при обращении к свойствам второй таблицы через JOIN

При попытке сделать JOIN запрос, свойства Tokens оказываются null:

                var user = await dbContext.Tokens
                    .Where(t => t.refresh_token == hashRefreshToken)
                    .Join(dbContext.Users, token => token.user_id, user => user.id, (token, user) => user)
                    .FirstOrDefaultAsync();

Хотя если сделать 2 отдельных запроса, значения null в token не будет:

var token = await dbContext.Tokens.FirstOrDefaultAsync(t => t.refresh_token == hashRefreshToken);

var user = await dbContext.Users.FindAsync(token.user_id);

Вот модель токена:

[Table("tokens")]
public class TokenModel
{
    [Key]
    public int token_id { get; set; }

    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public string? refresh_token { get; set; }

    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public DateTime? expiry_date { get; set; }

    [ForeignKey("user_id")]
    public int user_id { get; set; }

    [JsonIgnore]
    public UserModel User { get; set; }
}

Модель юзера:

[Table("users")]
public class UserModel
{
    [Key]
    [Required]
    public int id { get; set; }

    [Required]
    public string username { get; set; }

    [Required]
    public string role { get; set; }

    [EmailAddress]
    [Required]
    public string email { get; set; }

    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public string password_hash { get; set; }

    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public string? api_key { get; set; }

    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public UserQuotasModel Quotas { get; set; }

    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public UserKeyModel Keys { get; set; }

    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public ICollection<UserFileModel> Files { get; set; }

    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public TokenModel Tokens { get; set; }

    //[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    //public ApiModel API { get; set; }
}

Так настроены отношеия в modelBuilder:

        modelBuilder.Entity<UserModel>()
            .HasOne(u => u.Tokens)
            .WithOne(t => t.User)
            .HasForeignKey<TokenModel>(t => t.user_id)
            .OnDelete(DeleteBehavior.Cascade);

Ну DbSet:

public DbSet<TokenModel> Tokens { get; set; }

Вот вариант middleware в котором используется Join и свойства токена == null

    public async Task Invoke(HttpContext context, FileCryptDbContext dbContext, ITokenService tokenService)
    {
        if(context.Request.Cookies.TryGetValue("JwtToken", out string? JwtToken))
        {
            context.Request.Headers.Add("Authorization", $"Bearer {JwtToken}");
        }
        else
        {
            if(context.Request.Cookies.TryGetValue("RefreshToken", out string? RefreshToken))
            {
                var hashRefreshToken = tokenService.HashingToken(RefreshToken);
                //var token = await dbContext.Tokens.FirstOrDefaultAsync(t => t.refresh_token == hashRefreshToken);

                var user = await dbContext.Tokens
                    .Where(t => t.refresh_token == hashRefreshToken)
                    .Join(dbContext.Users, token => token.user_id, user => user.id, (token, user) => user)
                    .FirstOrDefaultAsync();

                if (user is not null && user.Tokens.expiry_date.HasValue)
                {
                    if (user.Tokens.expiry_date > DateTime.UtcNow)
                    {
                        //var user = await dbContext.Users.FindAsync(token.user_id);
                        var userModel = new UserModel { id = user.id, username = user.username, email = user.email, role = user.role };
                        string NewJwtToken = tokenService.GenerateJwtToken(userModel, 20);
                        var JwtCookieOptions = tokenService.SetCookieOptions(TimeSpan.FromMinutes(20));
                        context.Response.Cookies.Append("JwtToken", NewJwtToken, JwtCookieOptions);

                        context.Request.Headers.Add("Authorization", $"Bearer {NewJwtToken}");
                    }
                }
            }
        }
        await _next(context);
    }

Ну и вот вариант при котором используются 2 отдельных запроса, и все работает:

    public async Task Invoke(HttpContext context, FileCryptDbContext dbContext, ITokenService tokenService)
    {
        if(context.Request.Cookies.TryGetValue("JwtToken", out string? JwtToken))
        {
            context.Request.Headers.Add("Authorization", $"Bearer {JwtToken}");
        }
        else
        {
            if(context.Request.Cookies.TryGetValue("RefreshToken", out string? RefreshToken))
            {
                var hashRefreshToken = tokenService.HashingToken(RefreshToken);
                var token = await dbContext.Tokens.FirstOrDefaultAsync(t => t.refresh_token == hashRefreshToken);

                if (token is not null && token.expiry_date.HasValue)
                {
                    if (token.expiry_date > DateTime.UtcNow)
                    {
                        var user = await dbContext.Users.FindAsync(token.user_id);

                        var userModel = new UserModel { id = user.id, username = user.username, email = user.email, role = user.role };
                        string NewJwtToken = tokenService.GenerateJwtToken(userModel, 20);
                        var JwtCookieOptions = tokenService.SetCookieOptions(TimeSpan.FromMinutes(20));
                        context.Response.Cookies.Append("JwtToken", NewJwtToken, JwtCookieOptions);

                        context.Request.Headers.Add("Authorization", $"Bearer {NewJwtToken}");
                    }
                }
            }
        }
        await _next(context);
    }

Заметил что если использовать Include, то все работает:

                var user = await dbContext.Tokens
                    .Where(t => t.refresh_token == hashRefreshToken)
                    .Include(t => t.User)
                    .FirstOrDefaultAsync();

Ответы (2 шт):

Автор решения: Leon

Ну так правильно. Include подгружает связные данные док. Если вы хотите получить пользователя

var user = await dbContext.Tokens
                .Where(t => t.refresh_token == hashRefreshToken)
                .Include(t => t.User)
                .Select(t => t.User)
                .FirstOrDefaultAsync(); 

или

 var user = await dbContext.Users
                 .FirstOrDefaultAsync(t => t.Tokens.refresh_token == hashRefreshToken);
→ Ссылка
Автор решения: Alexander Petrov

У вас в запросе с Join явно указано вернуть лишь юзера:

(token, user) => user

Исправьте resultSelector на следующее выражение:

(token, user) => new { token, user }

и вернутся оба типа.

Переменная будет анонимного типа. Я бы назвал её tokenAndUser. Она будет иметь два свойства. Думаю, разберётесь.


Включите логирование сгенерированных SQL-запросов, чтобы можно было легче разбираться.

P.S. Нейминг у вас ужасный! Исправляйте.

→ Ссылка