using AsbCloudApp.Data; using AsbCloudApp.Exceptions; using AsbCloudApp.Repositories; using AsbCloudApp.Services; using AsbCloudDb; using AsbCloudDb.Model; using AsbCloudInfrastructure.EfCache; using Mapster; using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace AsbCloudInfrastructure.Repository { #nullable enable public class UserRepository : IUserRepository { private readonly IAsbCloudDbContext dbContext; private readonly IUserRoleRepository userRoleRepository; private const string userCacheTag = "User"; private const string relationUserUserRoleCacheTag = "RelationUserUserRole"; private static readonly TimeSpan cacheObsolence = TimeSpan.FromMinutes(15); private static readonly TypeAdapterConfig userTypeAdapterConfig = TypeAdapterConfig .NewConfig() .Ignore(dst => dst.Company, dst => dst.FileMarks, dst => dst.Files, dst => dst.RelationUsersUserRoles) .Config; public UserRepository(IAsbCloudDbContext dbContext, IUserRoleRepository userRoleRepository) { this.dbContext = dbContext; this.userRoleRepository = userRoleRepository; } public async Task InsertAsync(UserExtendedDto dto, CancellationToken token = default) { dto.Id = default; var entity = Convert(dto); await AssertLoginIsBusyAsync(dto.Login, token); var userRoles = await userRoleRepository.GetByNamesAsync(dto.RoleNames, token).ConfigureAwait(false); var updatedEntity = await dbContext.Users.AddAsync(entity, token).ConfigureAwait(false); if (userRoles?.Any() == true) await UpdateRolesCacheForUserAsync(updatedEntity.Entity.Id, userRoles, token); await dbContext.SaveChangesAsync(token); DropCacheUsers(); return updatedEntity?.Entity?.Id ?? 0; } public Task InsertRangeAsync(IEnumerable newItems, CancellationToken token = default) { throw new NotImplementedException(); } public async Task> GetAllAsync(CancellationToken token = default) { var entities = await GetCacheUserAsync(token); var dtos = entities.Select(Convert).ToList(); for (var i = 0; i < dtos.Count; i++) dtos[i].RoleNames = GetRolesNamesByIdUser(dtos[i].Id); return dtos; } public UserExtendedDto? GetOrDefault(int id) { var entity = GetCacheUser().FirstOrDefault(u => u.Id == id); if (entity is null) return null; var dto = Convert(entity); dto.RoleNames = GetRolesNamesByIdUser(dto.Id); return dto; } public async Task GetOrDefaultAsync(int id, CancellationToken token = default) { var entity = (await GetCacheUserAsync(token)).FirstOrDefault(u => u.Id == id); if (entity is null) return null; var dto = Convert(entity); dto.RoleNames = GetRolesNamesByIdUser(dto.Id); return dto; } public async Task UpdateAsync(UserExtendedDto dto, CancellationToken token = default) { if (dto.Id <= 1) throw new ArgumentInvalidException($"Invalid id {dto.Id}. You can't edit this user.", nameof(dto)); var oldUser = (await GetCacheUserAsync(token)).FirstOrDefault(u => u.Id == dto.Id); if (oldUser is null) return 0; if (oldUser.Login != dto.Login) await AssertLoginIsBusyAsync(dto.Login, token); var userRoles = await userRoleRepository.GetByNamesAsync(dto.RoleNames, token).ConfigureAwait(false); if (userRoles is not null) await UpdateRolesCacheForUserAsync(dto.Id, userRoles, token); var entity = Convert(dto); var result = dbContext.Users.Upsert(entity); await dbContext.SaveChangesAsync(token); DropCacheUsers(); return result?.Entity?.Id ?? 0; } public async Task DeleteAsync(int id, CancellationToken token = default) { var entity = (await GetCacheUserAsync(token)).FirstOrDefault(u => u.Id == id); if (entity is null) return 0; var result = dbContext.Users.Remove(entity); await dbContext.SaveChangesAsync(token); DropCacheUsers(); return result?.Entity?.Id ?? 0; } public async Task DeleteAsync(IEnumerable ids, CancellationToken token = default) { var entities = (await GetCacheUserAsync(token)).Where(r => ids.Contains(r.Id)); if (entities is null) return 0; var count = entities.Count(); dbContext.Users.RemoveRange(entities); await dbContext.SaveChangesAsync(token); DropCacheUsers(); return count; } public IEnumerable? GetRolesByIdUser(int idUser, int nestedLevel = 0) { var roles = GetCachRelationUserUserRoleCacheTag().Where(r => r.IdUser == idUser); if (roles is null) return null; return roles.SelectMany(r => userRoleRepository.GetNestedById(r.IdUserRole, nestedLevel)); } public IEnumerable? GetNestedPermissions(int idUser) { var roles = GetRolesByIdUser(idUser, 7); if (roles is null) return null; var permissions = roles .Where(r => r.Permissions is not null) .SelectMany(r => r.Permissions); return permissions; } public bool HasPermission(int idUser, string permissionName) { if (idUser == 1) return true; var relationsToRoles = GetCachRelationUserUserRoleCacheTag().Where(r => r.IdUser == idUser); if (relationsToRoles is null) return false; return userRoleRepository.HasPermission(relationsToRoles.Select(r => r.IdUserRole), permissionName); } private IEnumerable? GetRolesNamesByIdUser(int idUser) => GetRolesByIdUser(idUser) ?.Select(r => r.Caption) .Distinct(); private async Task AssertLoginIsBusyAsync(string login, CancellationToken token = default) { var existingUser = (await GetCacheUserAsync(token)) .FirstOrDefault(u => u.Login.ToLower() == login.ToLower()); if (existingUser is not null) throw new ArgumentInvalidException($"Login {login} is busy by {existingUser.MakeDisplayName()}, id{existingUser.Id}", nameof(login)); } private Task> GetCacheUserAsync(CancellationToken token) => dbContext.Users .Include(r => r.Company) .Include(r => r.RelationUsersUserRoles) .FromCacheAsync(userCacheTag, cacheObsolence, token); private IEnumerable GetCacheUser() => dbContext.Users .Include(r => r.Company) .Include(r => r.RelationUsersUserRoles) .FromCache(userCacheTag, cacheObsolence); private void DropCacheUsers() => dbContext.Users.DropCache(userCacheTag); private Task> GetCacheRelationUserUserRoleAsync(CancellationToken token) => dbContext.RelationUserUserRoles .Include(r => r.UserRole) .Include(r => r.User) .FromCacheAsync(relationUserUserRoleCacheTag, cacheObsolence, token); private IEnumerable GetCachRelationUserUserRoleCacheTag() => dbContext.RelationUserUserRoles .Include(r => r.UserRole) .Include(r => r.User) .FromCache(relationUserUserRoleCacheTag, cacheObsolence); private void DropCacheRelationUserUserRoleCacheTag() => dbContext.RelationUserUserRoles.DropCache(relationUserUserRoleCacheTag); private async Task UpdateRolesCacheForUserAsync(int idUser, IEnumerable newRoles, CancellationToken token) { var relations = (await GetCacheRelationUserUserRoleAsync(token)).Where(r => r.IdUser == idUser); dbContext.RelationUserUserRoles.RemoveRange(relations); if (newRoles?.Any() == true) await dbContext.RelationUserUserRoles.AddRangeAsync(newRoles.Select(role => new RelationUserUserRole { IdUser = idUser, IdUserRole = role.Id }), token).ConfigureAwait(false); await dbContext.SaveChangesAsync(token); DropCacheRelationUserUserRoleCacheTag(); } protected virtual User Convert(UserExtendedDto dto) { var entity = dto.Adapt(userTypeAdapterConfig); if (string.IsNullOrEmpty(entity.PasswordHash)) entity.PasswordHash = dbContext.Users.FirstOrDefault(u => u.Id == dto.Id)?.PasswordHash; return entity; } protected virtual UserExtendedDto Convert(User entity) { var dto = entity.Adapt(); return dto; } } #nullable disable }