using Microsoft.EntityFrameworkCore; using Modules.User.Application.Models.Access; using Modules.User.Application.Repositories; using Modules.User.Database.Database; using Modules.User.Database.Database.Entities; using Modules.User.Domain.Factories; using Modules.User.Domain.Factories.User; using Modules.User.Domain.ValueObjects; using ClientInfo = Modules.User.Domain.Entities.User.ClientInfo; namespace Modules.User.Database.Repositories; public class UserRepository(UserDbContext context) : IUserRepository { private IQueryable GetUserQuery(bool asNoTracking = true) { var query = context.Users .Include(q => q.Account) .ThenInclude(q => q.Sessions) .Include(q => q.Account) .ThenInclude(q => q.Bans) .Include(q => q.Account) .ThenInclude(q => q.Roles) .ThenInclude(q => q.Role) // .ThenInclude(q => q.Permissions) .Include(q => q.Account) .ThenInclude(q => q.Permissions) // .ThenInclude(q => q.Permission) .AsSplitQuery(); return asNoTracking ? query.AsNoTracking() : query; } public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) { var dbUser = await GetUserQuery() .FirstOrDefaultAsync(x => x.Id == id, cancellationToken); return dbUser == null ? null : MapToDomain(dbUser); } public async Task ExistsByEmailAsync(string email, CancellationToken cancellationToken) { return await context.Accounts .AnyAsync(q => q.Email == email.Trim().ToLower(), cancellationToken); } public async Task TryGetHashedAccountAsync(string email, CancellationToken cancellationToken) { var normalized = email.Trim().ToLowerInvariant(); var account = await context.Accounts .Where(a => a.Email == normalized) .Select(q => new HashedAccount(q.Id, q.HashedPassword)) .SingleOrDefaultAsync(cancellationToken); return account; } public async Task GetByAccountIdAsync(Guid accountId, CancellationToken cancellationToken = default) { var dbUser = await GetUserQuery() .FirstOrDefaultAsync(x => x.Account.Id == accountId, cancellationToken); return dbUser == null ? null : MapToDomain(dbUser); } public Task AddAsync(Domain.Entities.User.User user, CancellationToken cancellationToken = default) { var dbUser = MapToEfNew(user); context.Users.Add(dbUser); return Task.CompletedTask; } public async Task SaveAsync(Domain.Entities.User.User user, CancellationToken cancellationToken = default) { var dbUser = await GetUserQuery(asNoTracking: false) .FirstAsync(x => x.Id == user.Id, cancellationToken); await context.Entry(dbUser.Account) .Collection(a => a.Sessions) .Query() .Include(q => q.ClientInfo) .LoadAsync(cancellationToken); // await context.Entry(dbUser.Account).Collection(a => a.Roles).LoadAsync(cancellationToken); // await context.Entry(dbUser.Account).Collection(a => a.Permissions).LoadAsync(cancellationToken); // await context.Entry(dbUser.Account).Collection(a => a.Bans).LoadAsync(cancellationToken); // ApplyDomainToEf(user, dbUser); await ApplyDomainToEf(user, dbUser, cancellationToken); } // ---------- Mapping ---------- private static Domain.Entities.User.User MapToDomain(Database.Entities.User ef) { var email = Email.Create(ef.Account.Email); var sessions = ef.Account.Sessions .OrderBy(q => q.Id) .Select(q => SessionFactory.Load(q.Id, q.RefreshToken, ClientInfo.Create(q.ClientInfo.UserAgent, q.ClientInfo.Country, q.ClientInfo.Region), q.ExpiredDate, q.LastUpdate)); var roles = ef.Account.Roles .Where(q => !q.RevokedAtUtc.HasValue) .OrderByDescending(q => q.GrantedAtUtc) .Select(q => RoleGrantFactory.Load(q.RoleId, q.GrantedAtUtc, q.IssuerId, q.GrantReason, q.RevokedAtUtc, q.RevokerId, q.RevokeReason)); var permissions = ef.Account.Permissions .Where(q => !q.RevokedAtUtc.HasValue) .OrderByDescending(q => q.GrantedAtUtc) .Select(q => PermissionGrantFactory.Load(q.PermissionId, q.GrantedAtUtc, q.IssuerId, q.GrantReason, q.RevokedAtUtc, q.RevokerId, q.RevokeReason)); var bans = ef.Account.Bans .OrderByDescending(q => q.ReleaseDate).ThenByDescending(q => q.BanDate) .Select(q => BanFactory.Load(q.Id, q.Reason, q.BanDate, q.ReleaseDate, q.IssuerId, q.UnbanIssuerId, q.UnbanDate, q.UnbanReason, q.Deleted)); var account = AccountFactory.Load(ef.Account.Id, email, ef.Account.HashedPassword, sessions, roles, permissions, bans, ef.Account.Deleted); var user = UserFactory.Load(ef.Id, ef.NickName, ef.FirstName, ef.Patronymic, ef.LastName, ef.AvatarId, ef.RegionalSettings?.LanguageId, ef.BirthDate, account, ef.Deleted); return user; } private static Database.Entities.User MapToEfNew(Domain.Entities.User.User domain) { return new Database.Entities.User { Id = domain.Id, NickName = domain.NickName, FirstName = domain.FirstName, Patronymic = domain.Patronymic, LastName = domain.LastName, AvatarId = domain.Avatar?.Id, RegionalSettings = domain.Language is null ? null : new RegionalSettings { LanguageId = domain.Language.Id }, BirthDate = domain.BirthDate, Account = new Account { Id = domain.Account.Id, UserId = domain.Id, Email = domain.Account.Email.Value, HashedPassword = domain.Account.HashedPassword, Sessions = [..domain.Account.Sessions.Select(s => new Session { Id = s.Id, AccountId = domain.Account.Id, RefreshToken = s.RefreshToken, ClientInfo = new Database.Entities.ClientInfo { UserAgent = s.ClientInfo.UserAgent, Country = s.ClientInfo.Country, Region = s.ClientInfo.Region }, ExpiredDate = s.ExpiredDateUtc, LastUpdate = s.LastUpdateUtc })], Roles = [..domain.Account.RoleGrants.Select(r => new AccountRole { // Id = // AccountId = domain.Account.Id, RoleId = r.RoleId, IssuerId = r.IssuerId, GrantedAtUtc = r.GrantedAtUtc, RevokedAtUtc = null, })], Permissions = [..domain.Account.PermissionGrants.Select(p => new AccountPermission { // Id = // AccountId = domain.Account.Id, PermissionId = p.PermissionId, IssuerId = p.IssuerId, GrantedAtUtc = p.GrantedAtUtc, RevokedAtUtc = null, })], Bans = [..domain.Account.Bans.Select(b => new AccountBan { Id = b.Id, AccountId = domain.Account.Id, Reason = b.Reason, BanDate = b.BanDate, ReleaseDate = b.ReleaseDate, IssuerId = b.BanIssuerId, Deleted = b.Deleted, })], Deleted = domain.Account.Deleted, }, Deleted = domain.Deleted, }; } private async Task ApplyDomainToEf(Domain.Entities.User.User domain, Database.Entities.User ef, CancellationToken cancellationToken) { ef.NickName = domain.NickName; ef.FirstName = domain.FirstName; ef.Patronymic = domain.Patronymic; ef.LastName = domain.LastName; ef.AvatarId = domain.Avatar?.Id; ef.RegionalSettings ??= new RegionalSettings(); ef.RegionalSettings.LanguageId = domain.Language?.Id; ef.BirthDate = domain.BirthDate; ef.Deleted = domain.Deleted; ef.Account.Email = domain.Account.Email.Value; ef.Account.HashedPassword = domain.Account.HashedPassword; ef.Account.Deleted = domain.Account.Deleted; var efSessions = ef.Account.Sessions.ToDictionary(s => s.Id, s => s); var domainSessions = domain.Account.Sessions.ToDictionary(s => s.Id, s => s); foreach (var es in ef.Account.Sessions.ToList()) { if (!domainSessions.ContainsKey(es.Id)) ef.Account.Sessions.Remove(es); } // var keepIds = domain.Account.Sessions.Select(s => s.Id).ToArray(); // await context.Sessions // .Where(s => s.AccountId == ef.Account.Id && !keepIds.Contains(s.Id)) // .ExecuteDeleteAsync(cancellationToken); foreach (var ds in domain.Account.Sessions) { if (!efSessions.TryGetValue(ds.Id, out var es)) { es = new Session { Id = ds.Id, AccountId = ef.Account.Id, ClientInfo = new Database.Entities.ClientInfo { UserAgent = ds.ClientInfo.UserAgent, Country = ds.ClientInfo.Country, Region = ds.ClientInfo.Region, } }; // ef.Account.Sessions.Add(es); context.Sessions.Add(es); } es.RefreshToken = ds.RefreshToken; es.ExpiredDate = ds.ExpiredDateUtc; es.LastUpdate = ds.LastUpdateUtc; es.ClientInfo ??= new Database.Entities.ClientInfo(); es.ClientInfo.UserAgent = ds.ClientInfo.UserAgent; es.ClientInfo.Country = ds.ClientInfo.Country; es.ClientInfo.Region = ds.ClientInfo.Region; } var now = DateTime.UtcNow; var efRoles = ef.Account.Roles .Where(q => !q.RevokedAtUtc.HasValue) .ToDictionary(x => x.RoleId); var domainRoles = domain.Account.RoleGrants .Select(r => r.RoleId) .ToList(); foreach (var dg in domain.Account.RoleGrants) { if (!efRoles.TryGetValue(dg.RoleId, out var er)) { er = new AccountRole { AccountId = ef.Account.Id, RoleId = dg.RoleId, GrantedAtUtc = dg.GrantedAtUtc, IssuerId = dg.IssuerId, RevokedAtUtc = null, }; ef.Account.Roles.Add(er); } } foreach (var er in efRoles.Values) { if (!domainRoles.Contains(er.RoleId)) { er.RevokedAtUtc = now; } } var efPermissions = ef.Account.Permissions .Where(q => !q.RevokedAtUtc.HasValue) .ToDictionary(p => p.PermissionId); var domainPermissions = domain.Account.PermissionGrants .Select(r => r.PermissionId) .ToList(); foreach (var dg in domain.Account.PermissionGrants) { if (!efPermissions.TryGetValue(dg.PermissionId, out var ep)) { ep = new AccountPermission { AccountId = ef.Account.Id, PermissionId = dg.PermissionId, GrantedAtUtc = dg.GrantedAtUtc, IssuerId = dg.IssuerId, RevokedAtUtc = null, }; ef.Account.Permissions.Add(ep); } } foreach (var er in efPermissions.Values) { if (!domainPermissions.Contains(er.PermissionId)) { er.RevokedAtUtc = now; } } var efBans = ef.Account.Bans.ToDictionary(b => b.Id); foreach (var db in domain.Account.Bans) { if (!efBans.TryGetValue(db.Id, out var eb)) { eb = new AccountBan { Id = db.Id, AccountId = ef.Account.Id, IssuerId = db.BanIssuerId, }; ef.Account.Bans.Add(eb); } eb.Reason = db.Reason; eb.BanDate = db.BanDate; eb.ReleaseDate = db.ReleaseDate; eb.Deleted = db.Deleted; } } }