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 dtos = (await GetCacheUserAsync(token)).ToList(); if (dtos is null) return Enumerable.Empty(); for (var i = 0; i < dtos.Count; i++) dtos[i].RoleNames = GetRolesNamesByIdUser(dtos[i].Id); return dtos; } public UserExtendedDto? GetOrDefault(int id) { var dto = GetCacheUser().FirstOrDefault(u => u.Id == id); if (dto is null) return null; var entity = Convert(dto); dto.RoleNames = GetRolesNamesByIdUser(dto.Id); return dto; } public async Task GetOrDefaultAsync(int id, CancellationToken token) { var dto = (await GetCacheUserAsync(token)).FirstOrDefault(u => u.Id == id); if (dto is null) return null; 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 = Convert(dto); var local = dbContext.Set() .Local .FirstOrDefault(entry => entry.Id.Equals(entity.Id)); if (local != null) { dbContext.Entry(local).State = EntityState.Detached; } dbContext.Entry(entity).State = EntityState.Modified; await dbContext.SaveChangesAsync(token); DropCacheUsers(); return entity.Id; } public async Task DeleteAsync(int id, CancellationToken token) { var dto = (await GetCacheUserAsync(token)).FirstOrDefault(u => u.Id == id); if (dto is null) return 0; var entity = Convert(dto); var result = dbContext.Users.Remove(entity); 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 async Task> GetCacheUserAsync(CancellationToken token) { var query = dbContext.Users .Include(r => r.Company) .Include(r => r.RelationUsersUserRoles); return await FromCacheAsync(query, userCacheTag, cacheObsolence, Convert, token); } private IEnumerable GetCacheUser() { var query = dbContext.Users .Include(r => r.Company) .Include(r => r.RelationUsersUserRoles); return FromCache(query, userCacheTag, cacheObsolence, Convert); } private void DropCacheUsers() { memoryCache.Remove(userCacheTag); } private async Task> GetCacheRelationUserUserRoleAsync(CancellationToken token) { var query = dbContext.RelationUserUserRoles .Include(r => r.UserRole) .Include(r => r.User); return await FromCacheAsync(query, relationUserUserRoleCacheTag, cacheObsolence, token); } private IEnumerable GetCachRelationUserUserRoleCacheTag() { var query = dbContext.RelationUserUserRoles .Include(r => r.UserRole) .Include(r => r.User); return FromCache(query, relationUserUserRoleCacheTag, cacheObsolence); } private void DropCacheRelationUserUserRoleCacheTag() { memoryCache.Remove(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(); } public async Task> FromCacheAsync(IQueryable query, string tag, TimeSpan obsolescence, Func convert, CancellationToken token) where TEntity : class { async Task factory(CancellationToken token) => await query.AsNoTracking().ToArrayAsync(token); var cache = await GetOrAddCacheAsync(tag, factory, obsolescence, token); return cache.Select(convert); } public async Task> FromCacheAsync(IQueryable query, string tag, TimeSpan obsolescence, CancellationToken token) where TEntity : class { async Task factory(CancellationToken token) => await query.AsNoTracking().ToArrayAsync(token); var cache = await GetOrAddCacheAsync(tag, factory, obsolescence, token); return cache; } public IEnumerable FromCache(IQueryable query, string tag, TimeSpan obsolescence, Func convert) where TEntity : class { TEntity[] factory() => query.AsNoTracking().ToArray(); var cache = GetOrAddCache(tag, factory, obsolescence); return cache.Select(convert); } public IEnumerable FromCache(IQueryable query, string tag, TimeSpan obsolescence) where TEntity : class { TEntity[] factory() => query.AsNoTracking().ToArray(); var cache = GetOrAddCache(tag, factory, obsolescence); return cache; } private async Task GetOrAddCacheAsync(string tag, Func> valueFactoryAsync, TimeSpan obsolete, CancellationToken token) { memoryCache.TryGetValue(tag, out TEntity[]? cached); if (cached == null) { var values = await valueFactoryAsync(token); if (values != null) { memoryCache.Set(tag, values, new MemoryCacheEntryOptions().SetAbsoluteExpiration(obsolete)); } return values!; } return cached; } private TEntity[] GetOrAddCache(string tag, Func valueFactory, TimeSpan obsolete) { memoryCache.TryGetValue(tag, out TEntity[]? cached); if (cached == null) { var values = valueFactory(); if (values != null) { memoryCache.Set(tag, values, new MemoryCacheEntryOptions().SetAbsoluteExpiration(obsolete)); } return values!; } return cached; } 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 }