using AsbCloudApp.Data; using AsbCloudApp.Exceptions; using AsbCloudApp.Repositories; using AsbCloudDb.Model; using Mapster; using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Memory; 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; private readonly IMemoryCache memoryCache; public UserRepository(IAsbCloudDbContext dbContext, IUserRoleRepository userRoleRepository, IMemoryCache memoryCache) { this.dbContext = dbContext; this.userRoleRepository = userRoleRepository; this.memoryCache = memoryCache; } public async Task InsertAsync(UserExtendedDto dto, CancellationToken token) { 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); await dbContext.SaveChangesAsync(token); if (userRoles?.Any() == true) await UpdateRolesCacheForUserAsync(updatedEntity.Entity.Id, userRoles, token); DropCacheUsers(); return updatedEntity.Entity.Id; } public Task InsertRangeAsync(IEnumerable newItems, CancellationToken token) { throw new NotImplementedException(); } public async Task> GetAllAsync(CancellationToken token) { var users = await GetCacheUserAsync(token); if (users is null) return Enumerable.Empty(); var dtos = users .Select(d => Convert(d)); foreach(var dto in dtos) { dto.RoleNames = GetRolesNamesByIdUser(dto.Id); }; return dtos; } public UserExtendedDto? GetOrDefault(int id) { var user = GetCacheUser().FirstOrDefault(u => u.Id == id); if (user is null) return null; var dto = Convert(user); dto.RoleNames = GetRolesNamesByIdUser(dto.Id); return dto; } public async Task GetOrDefaultAsync(int id, CancellationToken token) { var user = (await GetCacheUserAsync(token)).FirstOrDefault(u => u.Id == id); if (user is null) return null; var dto = Convert(user); dto.RoleNames = GetRolesNamesByIdUser(dto.Id); return dto; } public async Task UpdateAsync(UserExtendedDto dto, CancellationToken token) { 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); await UpdateRolesCacheForUserAsync(dto.Id, userRoles, token); var entity = dbContext.Users.FirstOrDefault(u => u.Id == dto.Id); if (entity is not null) { var user = Convert(dto); entity.Id = user.Id; entity.Company = user.Company; entity.Name = user.Name; entity.Email = user.Email; entity.Phone = user.Phone; entity.Surname = user.Surname; entity.Patronymic = user.Patronymic; entity.Position = user.Position; entity.IdCompany = user.IdCompany; entity.IdState = user.IdState; entity.RelationUsersUserRoles = user.RelationUsersUserRoles; await dbContext.SaveChangesAsync(token); } DropCacheUsers(); return entity!.Id; } public async Task DeleteAsync(int id, CancellationToken token) { var user = (await GetCacheUserAsync(token)).FirstOrDefault(u => u.Id == id); if (user is null) return 0; var result = dbContext.Users.Remove(user); await dbContext.SaveChangesAsync(token); DropCacheUsers(); return result.Entity.Id; } public IEnumerable GetRolesByIdUser(int idUser, int nestedLevel = 0) { var roles = GetCachRelationUserUserRoleCacheTag().Where(r => r.IdUser == idUser); return roles.SelectMany(r => userRoleRepository.GetNestedById(r.IdUserRole, nestedLevel)); } public IEnumerable GetNestedPermissions(int idUser) { var roles = GetRolesByIdUser(idUser, 7); if (roles is null) return Enumerable.Empty(); 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, 7) .Select(r => r.Caption) .Distinct(); private async Task AssertLoginIsBusyAsync(string login, CancellationToken token) { var existingUserDto = (await GetCacheUserAsync(token)) .FirstOrDefault(u => u.Login.ToLower() == login.ToLower()); if (existingUserDto is not null) throw new ArgumentInvalidException($"Login {login} is busy by {existingUserDto.MakeDisplayName()}, id{existingUserDto.Id}", nameof(login)); } private IEnumerable GetCachRelationUserUserRoleCacheTag() { var cache = memoryCache.GetOrCreate(userCacheTag, cacheEntry => { cacheEntry.AbsoluteExpirationRelativeToNow = cacheObsolence; cacheEntry.SlidingExpiration = cacheObsolence; var query = dbContext.RelationUserUserRoles .Include(r => r.UserRole) .Include(r => r.User); var entities = query.ToArray(); return entities; }); return cache; } private void DropCacheRelationUserUserRoleCacheTag() { memoryCache.Remove(relationUserUserRoleCacheTag); } private async Task UpdateRolesCacheForUserAsync(int idUser, IEnumerable newRoles, CancellationToken token) { var relations = dbContext.RelationUserUserRoles.Where(r => r.IdUser == idUser); dbContext.RelationUserUserRoles.RemoveRange(relations); if (newRoles?.Any() is true) await dbContext.RelationUserUserRoles.AddRangeAsync(newRoles.Select(role => new RelationUserUserRole { IdUser = idUser, IdUserRole = role.Id }), token).ConfigureAwait(false); await dbContext.SaveChangesAsync(token); DropCacheRelationUserUserRoleCacheTag(); } private void DropCacheUsers() => memoryCache.Remove(userCacheTag); private IEnumerable GetCacheUser() { var cache = memoryCache.GetOrCreate(userCacheTag, cacheEntry => { cacheEntry.AbsoluteExpirationRelativeToNow = cacheObsolence; cacheEntry.SlidingExpiration = cacheObsolence; var query = dbContext.Users .Include(r => r.Company) .Include(r => r.RelationUsersUserRoles); var entities = query.ToArray(); return entities; }); return cache; } private Task> GetCacheUserAsync(CancellationToken token) { var cache = memoryCache.GetOrCreateAsync(userCacheTag, async (cacheEntry) => { cacheEntry.AbsoluteExpirationRelativeToNow = cacheObsolence; cacheEntry.SlidingExpiration = cacheObsolence; var query = dbContext.Users .Include(r => r.Company) .Include(r => r.RelationUsersUserRoles); var entities = await query.ToArrayAsync(token); return entities.AsEnumerable(); }); return cache; } protected 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 }