using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Dynamic.Core;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Logging;
using Sozsoft.Languages;
using Sozsoft.Languages.Entities;
using Sozsoft.Languages.Languages;
using Sozsoft.Platform.Entities;
using Sozsoft.Platform.Extensions;
using Sozsoft.Platform.ListForms;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.PermissionManagement;
using Volo.Abp.TenantManagement;
using static Sozsoft.Platform.Data.Seeds.SeedConsts;
namespace Sozsoft.Platform.Menus;
[Authorize]
public class MenuAppService : CrudAppService<
Menu,
MenuDto,
Guid,
PagedAndSortedResultRequestDto>
{
private readonly IRepository
_menuRepository;
private readonly IRepository _repositoryKey;
private readonly IRepository _repositoryText;
private readonly ITenantRepository _tenantRepository;
private readonly IPermissionDefinitionRecordRepository _permissionRepository;
private readonly LanguageTextAppService _languageTextAppService;
private readonly IRepository _moduleRepository;
private readonly IListFormSeedDataSynchronizer _seedDataSynchronizer;
public MenuAppService(
IRepository menuRepository,
IRepository languageKeyRepository,
IRepository languageTextRepository,
ITenantRepository tenantRepository,
IPermissionDefinitionRecordRepository permissionRepository,
LanguageTextAppService languageTextAppService,
IRepository moduleRepository,
IListFormSeedDataSynchronizer seedDataSynchronizer
) : base(menuRepository)
{
_menuRepository = menuRepository;
_repositoryKey = languageKeyRepository;
_repositoryText = languageTextRepository;
_tenantRepository = tenantRepository;
_permissionRepository = permissionRepository;
_languageTextAppService = languageTextAppService;
_moduleRepository = moduleRepository;
_seedDataSynchronizer = seedDataSynchronizer;
CreatePolicyName = $"{AppCodes.Menus.Menu}.Create";
UpdatePolicyName = $"{AppCodes.Menus.Menu}.Update";
DeletePolicyName = $"{AppCodes.Menus.Menu}.Delete";
}
public override async Task> GetListAsync(PagedAndSortedResultRequestDto input)
{
await CheckGetListPolicyAsync();
var query = await CreateFilteredQueryAsync(input);
query = query.Where(a => !a.IsDisabled);
//Tenant üzerinden modül bilgisi alınıp yalnızca o modüllerin menüleri listelenecek
if (CurrentTenant.Id is Guid tenantId)
{
var tenant = await _tenantRepository.FindAsync(tenantId);
if (tenant != null)
{
var tenantModules = tenant.GetModules();
if (tenantModules.Length > 0)
{
var permissionRecords = await _permissionRepository.GetListAsync();
var allowedPermissionNames = permissionRecords
.Where(p => p.GetModules().Intersect(tenantModules).Any())
.Select(p => p.Name)
.Distinct()
.ToList();
query = query.Where(menu =>
string.IsNullOrEmpty(menu.RequiredPermissionName) ||
allowedPermissionNames.Contains(menu.RequiredPermissionName));
}
}
}
var totalCount = await AsyncExecuter.CountAsync(query);
var entities = new List();
var entityDtos = new List();
if (totalCount > 0)
{
query = ApplySorting(query, input);
query = ApplyPaging(query, input);
var items = await AsyncExecuter.ToListAsync(query);
// Tüm unique permission'ları topla
var uniquePermissions = items
.Where(x => !string.IsNullOrWhiteSpace(x.RequiredPermissionName))
.Select(x => x.RequiredPermissionName)
.Distinct()
.ToList();
// Tüm permission'ları bir kerede kontrol et (N+1 query problemini önler)
var grantedPermissions = new HashSet();
foreach (var permission in uniquePermissions)
{
try
{
if (await AuthorizationService.IsGrantedAsync(permission))
{
grantedPermissions.Add(permission);
}
}
catch (Exception ex)
{
Logger.LogError(ex, $"Permission error for {permission}: {ex.Message}");
}
}
// Sadece yetkili menüleri filtrele
entities = items
.Where(item => string.IsNullOrWhiteSpace(item.RequiredPermissionName) ||
grantedPermissions.Contains(item.RequiredPermissionName))
.ToList();
entityDtos = await base.MapToGetListOutputDtosAsync(entities);
var languageKeys = entityDtos
.Select(item => string.IsNullOrWhiteSpace(item.DisplayName) ? item.Code : item.DisplayName)
.OfType()
.Where(key => !string.IsNullOrWhiteSpace(key))
.Distinct()
.ToList();
var languageTexts = await _repositoryText.GetListAsync(text =>
text.ResourceName == PlatformConsts.AppName &&
text.Key != null && languageKeys.Contains(text.Key) &&
(text.CultureName == PlatformConsts.DefaultLanguage ||
text.CultureName == LanguageCodes.Tr));
foreach (var menu in entityDtos)
{
var languageKey = string.IsNullOrWhiteSpace(menu.DisplayName)
? menu.Code
: menu.DisplayName;
menu.MenuTextEn = languageTexts.FirstOrDefault(text =>
text.Key == languageKey &&
text.CultureName == PlatformConsts.DefaultLanguage)?.Value;
menu.MenuTextTr = languageTexts.FirstOrDefault(text =>
text.Key == languageKey &&
text.CultureName == LanguageCodes.Tr)?.Value;
}
}
return new PagedResultDto(
totalCount,
entityDtos
);
}
public override async Task CreateAsync(MenuDto input)
{
await CheckCreatePolicyAsync();
await EnsureModuleAsync(input.ModuleId);
var keyExists = await _repositoryKey.AnyAsync(
a => a.Key == input.DisplayName &&
a.ResourceName == PlatformConsts.AppName);
if (!keyExists)
{
await _repositoryKey.InsertAsync(new LanguageKey
{
Key = input.DisplayName,
ResourceName = PlatformConsts.AppName
});
}
var result = await base.CreateAsync(input);
await SyncMenuRowsAsync([result.Code], SeedSyncOperation.Insert);
return result;
}
public override async Task UpdateAsync(Guid id, MenuDto input)
{
await CheckUpdatePolicyAsync();
await EnsureModuleAsync(input.ModuleId);
var key = await _repositoryKey.FirstOrDefaultAsync(
a => a.Key == input.DisplayName &&
a.ResourceName == PlatformConsts.AppName);
if (key is null)
{
// Yeni Key'i olustur
await _repositoryKey.InsertAsync(new LanguageKey
{
Key = input.DisplayName,
ResourceName = PlatformConsts.AppName
});
}
var previousCode = (await _menuRepository.GetAsync(id)).Code;
var result = await base.UpdateAsync(id, input);
await SyncUpdatedMenuRowsAsync([(previousCode, result.Code)]);
return result;
}
public async Task> UpdateAllAsync(List inputs)
{
await CheckUpdatePolicyAsync();
if (!inputs.Any())
return new List();
// Id kontrolü
if (inputs.Any(x => x.Id == Guid.Empty))
{
throw new ArgumentException("MenuDto içinde geçerli bir Id bulunmalıdır.");
}
// Tüm DisplayName'leri topla
var displayNames = inputs.Select(x => x.DisplayName).Distinct().ToList();
// Mevcut key'leri tek sorguda getir
var existingKeys = await AsyncExecuter.ToListAsync(
(await _repositoryKey.GetQueryableAsync())
.Where(a => displayNames.Contains(a.Key) && a.ResourceName == PlatformConsts.AppName));
var existingKeyNames = existingKeys.Select(x => x.Key).ToHashSet();
// Eksik key'leri toplu ekle
var newKeys = displayNames
.Where(name => !existingKeyNames.Contains(name))
.Select(name => new LanguageKey
{
Key = name,
ResourceName = PlatformConsts.AppName
})
.ToList();
if (newKeys.Any())
{
await _repositoryKey.InsertManyAsync(newKeys, autoSave: true);
}
var ids = inputs.Select(x => x.Id).ToList();
var previousCodes = (await _menuRepository.GetListAsync(a => ids.Contains(a.Id)))
.ToDictionary(a => a.Id, a => a.Code);
// Menüleri güncelle
var result = new List();
foreach (var input in inputs)
{
var updated = await base.UpdateAsync(input.Id, input);
result.Add(updated);
}
await SyncUpdatedMenuRowsAsync(result.Select(x => (previousCodes.GetValueOrDefault(x.Id), x.Code)));
return result;
}
public override async Task DeleteAsync(Guid id)
{
await CheckDeletePolicyAsync();
var menu = await _menuRepository.GetAsync(id);
if (menu != null)
{
await _repositoryKey.DeleteAsync(
a => a.Key == menu.DisplayName &&
a.ResourceName == PlatformConsts.AppName);
}
await base.DeleteAsync(id);
if (menu != null)
await _seedDataSynchronizer.RemoveRowsAsync(AppCodes.Menus.Menu, [menu.Code]);
}
public async Task> GetListMainMenuAsync()
{
await CheckGetPolicyAsync();
var query = await _menuRepository.GetQueryableAsync();
// Public site menusu yonetim kenar cubugunda yer almaz; o agac ziyaretci
// duzenine aittir ve uygulama konfigurasyonu uzerinden tasinir.
query = query.Where(a => !a.IsDisabled && a.ModuleId != null &&
a.ModuleId != PlatformConsts.PublicSite.ModuleId);
var entities = await AsyncExecuter.ToListAsync(query);
var entityDtos = await MapToGetListOutputDtosAsync(entities);
return entityDtos;
}
///
/// Menüyü tüm alanlarıyla günceller ve görünen adın dil metinlerini de yazar.
/// Kod (Code) dil anahtarı ve yetki eşleşmesi olduğu için değiştirilemez; gönderilen
/// değer yok sayılıp mevcut kod korunur.
///
public async Task UpdateWithLanguageKeyTextAsync(Guid id, MenuDto input)
{
await CheckUpdatePolicyAsync();
await EnsureModuleAsync(input.ModuleId);
var menu = await _menuRepository.GetAsync(id);
// Kod değiştirilemez: dil anahtarı, alt menülerin ParentCode'u ve permission
// kayıtları bu değere bağlıdır.
input.Code = menu.Code;
await UpsertMenuLanguageTextsAsync(menu.Code, input.MenuTextEn, input.MenuTextTr);
var result = await base.UpdateAsync(id, input);
await SyncMenuRowsAsync([result.Code], SeedSyncOperation.Update);
return result;
}
///
/// Menü koduna karşılık gelen dil anahtarını ve EN/TR metinlerini oluşturur veya günceller.
///
private async Task UpsertMenuLanguageTextsAsync(string code, string menuTextEn, string menuTextTr)
{
var keyExists = await _repositoryKey.AnyAsync(
a => a.Key == code &&
a.ResourceName == PlatformConsts.AppName);
if (!keyExists)
{
await _repositoryKey.InsertAsync(new LanguageKey
{
Key = code,
ResourceName = PlatformConsts.AppName
}, autoSave: true);
}
var changedTextIds = new List();
var insertedTextIds = new List();
foreach (var (culture, value) in new[] { ("en", menuTextEn), ("tr", menuTextTr) })
{
if (value == null)
continue;
var existing = await _repositoryText.FirstOrDefaultAsync(
a => a.CultureName == culture &&
a.Key == code &&
a.ResourceName == PlatformConsts.AppName);
if (existing != null)
{
existing.Value = value;
await _repositoryText.UpdateAsync(existing, autoSave: true);
changedTextIds.Add(existing.Id);
}
else
{
var inserted = await _repositoryText.InsertAsync(new LanguageText
{
Key = code,
CultureName = culture,
Value = value,
ResourceName = PlatformConsts.AppName
}, autoSave: true);
insertedTextIds.Add(inserted.Id);
}
}
await _languageTextAppService.ClearRedisCacheAsync();
// Dil metinleri de kendi ekraninin seed dosyasinda yasar; menu adini runtime'da
// degistirmek DB sifirlandiginda geri gelmeli.
await _seedDataSynchronizer.SyncRowsAsync(AppCodes.Languages.LanguageText, changedTextIds, SeedSyncOperation.Update);
await _seedDataSynchronizer.SyncRowsAsync(AppCodes.Languages.LanguageText, insertedTextIds, SeedSyncOperation.Insert);
}
public async Task CreateWithLanguageKeyTextAsync(MenuDto input)
{
await CheckCreatePolicyAsync();
await EnsureModuleAsync(input.ModuleId);
await UpsertMenuLanguageTextsAsync(input.Code, input.MenuTextEn, input.MenuTextTr);
var result = await base.CreateAsync(input);
await SyncMenuRowsAsync([result.Code], SeedSyncOperation.Insert);
return result;
}
///
/// Menu Yoneticisi de App.Menus.Menu ekraniyla ayni seed dosyasini yazar; aksi halde
/// siralama ve duzenlemeler DB sifirlandiginda kaybolur. Dosyanin anahtari Code 'dur.
///
private Task SyncMenuRowsAsync(IReadOnlyCollection codes, SeedSyncOperation operation) =>
_seedDataSynchronizer.SyncRowsAsync(AppCodes.Menus.Menu, [.. codes], operation);
///
/// Kodu degisen menunun eski satiri dosyadan cikarilir; dosya Code anahtarli oldugu icin
/// yeni kod ayri bir satir olarak eklenir.
///
private async Task SyncUpdatedMenuRowsAsync(IEnumerable<(string? PreviousCode, string Code)> changes)
{
var list = changes.ToList();
var renamed = list
.Where(x => x.PreviousCode != x.Code)
.Select(x => x.PreviousCode)
.OfType()
.Where(code => !string.IsNullOrWhiteSpace(code))
.Cast()
.ToArray();
if (renamed.Length > 0)
await _seedDataSynchronizer.RemoveRowsAsync(AppCodes.Menus.Menu, renamed);
await SyncMenuRowsAsync([.. list.Select(x => x.Code)], SeedSyncOperation.Update);
}
///
/// Menude secilen modul tanimli degilse olusturur; kok menude girilen kisa kod
/// dogrudan yeni bir modul olur ve ModuleId yabanci anahtari bos kalmaz.
///
private async Task EnsureModuleAsync(string moduleId)
{
var cleaned = moduleId?.Trim();
if (string.IsNullOrWhiteSpace(cleaned))
{
return;
}
if (await _moduleRepository.AnyAsync(a => a.Id == cleaned))
{
return;
}
await _moduleRepository.InsertAsync(new Module(cleaned) { Name = cleaned }, autoSave: true);
}
public async Task> GetModulesAsync()
{
var modules = await _moduleRepository.GetListAsync();
return modules
.OrderBy(module => module.Name)
.Select(module => new LookupDataDto
{
Key = module.Id,
Name = module.Name
})
.ToList();
}
}