diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo.Abp.TenantManagement.Domain.csproj b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo.Abp.TenantManagement.Domain.csproj index dc168af444..5de64b7a1d 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo.Abp.TenantManagement.Domain.csproj +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo.Abp.TenantManagement.Domain.csproj @@ -21,6 +21,7 @@ + diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo/Abp/TenantManagement/AbpTenantManagementDomainModule.cs b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo/Abp/TenantManagement/AbpTenantManagementDomainModule.cs index 764e9f7209..076c8b7a67 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo/Abp/TenantManagement/AbpTenantManagementDomainModule.cs +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo/Abp/TenantManagement/AbpTenantManagementDomainModule.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using Volo.Abp.AutoMapper; +using Volo.Abp.Caching; using Volo.Abp.Data; using Volo.Abp.Domain; using Volo.Abp.Domain.Entities.Events.Distributed; @@ -16,6 +17,7 @@ namespace Volo.Abp.TenantManagement [DependsOn(typeof(AbpDataModule))] [DependsOn(typeof(AbpDddDomainModule))] [DependsOn(typeof(AbpAutoMapperModule))] + [DependsOn(typeof(AbpCachingModule))] public class AbpTenantManagementDomainModule : AbpModule { private static readonly OneTimeRunner OneTimeRunner = new OneTimeRunner(); diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo/Abp/TenantManagement/TenantCacheItem.cs b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo/Abp/TenantManagement/TenantCacheItem.cs new file mode 100644 index 0000000000..94f898bbe5 --- /dev/null +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo/Abp/TenantManagement/TenantCacheItem.cs @@ -0,0 +1,36 @@ +using System; +using Volo.Abp.MultiTenancy; + +namespace Volo.Abp.TenantManagement +{ + [Serializable] + [IgnoreMultiTenancy] + public class TenantCacheItem + { + private const string CacheKeyFormat = "i:{0},n:{1}"; + + public TenantConfiguration Value { get; set; } + + public TenantCacheItem() + { + + } + + public TenantCacheItem(TenantConfiguration value) + { + Value = value; + } + + public static string CalculateCacheKey(Guid? id, string name) + { + if (id == null && name.IsNullOrWhiteSpace()) + { + throw new AbpException("Both id and name can't be invalid."); + } + + return string.Format(CacheKeyFormat, + id?.ToString() ?? "null", + (name.IsNullOrWhiteSpace() ? "null" : name)); + } + } +} diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo/Abp/TenantManagement/TenantCacheItemInvalidator.cs b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo/Abp/TenantManagement/TenantCacheItemInvalidator.cs new file mode 100644 index 0000000000..0416dd8df2 --- /dev/null +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo/Abp/TenantManagement/TenantCacheItemInvalidator.cs @@ -0,0 +1,24 @@ +using System.Threading.Tasks; +using Volo.Abp.Caching; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Entities.Events; +using Volo.Abp.EventBus; + +namespace Volo.Abp.TenantManagement +{ + public class TenantCacheItemInvalidator : ILocalEventHandler>, ITransientDependency + { + protected IDistributedCache Cache { get; } + + public TenantCacheItemInvalidator(IDistributedCache cache) + { + Cache = cache; + } + + public virtual async Task HandleEventAsync(EntityChangedEventData eventData) + { + await Cache.RemoveAsync(TenantCacheItem.CalculateCacheKey(eventData.Entity.Id, null)); + await Cache.RemoveAsync(TenantCacheItem.CalculateCacheKey(null, eventData.Entity.Name)); + } + } +} diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo/Abp/TenantManagement/TenantStore.cs b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo/Abp/TenantManagement/TenantStore.cs index 00b9ef0963..54e07a55fb 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo/Abp/TenantManagement/TenantStore.cs +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo/Abp/TenantManagement/TenantStore.cs @@ -1,5 +1,7 @@ using System; using System.Threading.Tasks; +using JetBrains.Annotations; +using Volo.Abp.Caching; using Volo.Abp.DependencyInjection; using Volo.Abp.MultiTenancy; using Volo.Abp.ObjectMapping; @@ -13,73 +15,125 @@ namespace Volo.Abp.TenantManagement protected ITenantRepository TenantRepository { get; } protected IObjectMapper ObjectMapper { get; } protected ICurrentTenant CurrentTenant { get; } + protected IDistributedCache Cache { get; } public TenantStore( ITenantRepository tenantRepository, IObjectMapper objectMapper, - ICurrentTenant currentTenant) + ICurrentTenant currentTenant, + IDistributedCache cache) { TenantRepository = tenantRepository; ObjectMapper = objectMapper; CurrentTenant = currentTenant; + Cache = cache; } public virtual async Task FindAsync(string name) { - using (CurrentTenant.Change(null)) //TODO: No need this if we can implement to define host side (or tenant-independent) entities! - { - var tenant = await TenantRepository.FindByNameAsync(name); - if (tenant == null) - { - return null; - } - - return ObjectMapper.Map(tenant); - } + return (await GetCacheItemAsync(null, name)).Value; } public virtual async Task FindAsync(Guid id) { - using (CurrentTenant.Change(null)) //TODO: No need this if we can implement to define host side (or tenant-independent) entities! - { - var tenant = await TenantRepository.FindAsync(id); - if (tenant == null) - { - return null; - } - - return ObjectMapper.Map(tenant); - } + return (await GetCacheItemAsync(id, null)).Value; } [Obsolete("Use FindAsync method.")] public virtual TenantConfiguration Find(string name) { - using (CurrentTenant.Change(null)) //TODO: No need this if we can implement to define host side (or tenant-independent) entities! + return (GetCacheItem(null, name)).Value; + } + + [Obsolete("Use FindAsync method.")] + public virtual TenantConfiguration Find(Guid id) + { + return (GetCacheItem(id, null)).Value; + } + + protected virtual async Task GetCacheItemAsync(Guid? id, string name) + { + var cacheKey = CalculateCacheKey(id, name); + + var cacheItem = await Cache.GetAsync(cacheKey, considerUow: true); + if (cacheItem != null) + { + return cacheItem; + } + + if (id.HasValue) { - var tenant = TenantRepository.FindByName(name); - if (tenant == null) + using (CurrentTenant.Change(null)) //TODO: No need this if we can implement to define host side (or tenant-independent) entities! { - return null; + var tenant = await TenantRepository.FindAsync(id.Value); + return await SetCacheAsync(cacheKey, tenant); } + } - return ObjectMapper.Map(tenant); + if (!name.IsNullOrWhiteSpace()) + { + using (CurrentTenant.Change(null)) //TODO: No need this if we can implement to define host side (or tenant-independent) entities! + { + var tenant = await TenantRepository.FindByNameAsync(name); + return await SetCacheAsync(cacheKey, tenant); + } } + + throw new AbpException("Both id and name can't be invalid."); } - [Obsolete("Use FindAsync method.")] - public virtual TenantConfiguration Find(Guid id) + protected virtual async Task SetCacheAsync(string cacheKey, [CanBeNull]Tenant tenant) + { + var tenantConfiguration = tenant != null ? ObjectMapper.Map(tenant) : null; + var cacheItem = new TenantCacheItem(tenantConfiguration); + await Cache.SetAsync(cacheKey, cacheItem, considerUow: true); + return cacheItem; + } + + [Obsolete("Use GetCacheItemAsync method.")] + protected virtual TenantCacheItem GetCacheItem(Guid? id, string name) { - using (CurrentTenant.Change(null)) //TODO: No need this if we can implement to define host side (or tenant-independent) entities! + var cacheKey = CalculateCacheKey(id, name); + + var cacheItem = Cache.Get(cacheKey, considerUow: true); + if (cacheItem != null) + { + return cacheItem; + } + + if (id.HasValue) { - var tenant = TenantRepository.FindById(id); - if (tenant == null) + using (CurrentTenant.Change(null)) //TODO: No need this if we can implement to define host side (or tenant-independent) entities! { - return null; + var tenant = TenantRepository.FindById(id.Value); + return SetCache(cacheKey, tenant); } + } - return ObjectMapper.Map(tenant); + if (!name.IsNullOrWhiteSpace()) + { + using (CurrentTenant.Change(null)) //TODO: No need this if we can implement to define host side (or tenant-independent) entities! + { + var tenant = TenantRepository.FindByName(name); + return SetCache(cacheKey, tenant); + } } + + throw new AbpException("Both id and name can't be invalid."); + } + + [Obsolete("Use SetCacheAsync method.")] + protected virtual TenantCacheItem SetCache(string cacheKey, [CanBeNull]Tenant tenant) + { + var tenantConfiguration = tenant != null ? ObjectMapper.Map(tenant) : null; + var cacheItem = new TenantCacheItem(tenantConfiguration); + Cache.Set(cacheKey, cacheItem, considerUow: true); + return cacheItem; + } + + protected virtual string CalculateCacheKey(Guid? id, string name) + { + return TenantCacheItem.CalculateCacheKey(id, name); } } } diff --git a/modules/tenant-management/test/Volo.Abp.TenantManagement.Domain.Tests/Volo/Abp/TenantManagement/TenantCacheItemInvalidator_Tests.cs b/modules/tenant-management/test/Volo.Abp.TenantManagement.Domain.Tests/Volo/Abp/TenantManagement/TenantCacheItemInvalidator_Tests.cs new file mode 100644 index 0000000000..4c2a8e3c3f --- /dev/null +++ b/modules/tenant-management/test/Volo.Abp.TenantManagement.Domain.Tests/Volo/Abp/TenantManagement/TenantCacheItemInvalidator_Tests.cs @@ -0,0 +1,86 @@ +using System.Threading.Tasks; +using Shouldly; +using Volo.Abp.Caching; +using Volo.Abp.MultiTenancy; +using Xunit; + +namespace Volo.Abp.TenantManagement +{ + public class TenantCacheItemInvalidator_Tests : AbpTenantManagementDomainTestBase + { + private readonly IDistributedCache _cache; + private readonly ITenantStore _tenantStore; + private readonly ITenantRepository _tenantRepository; + + public TenantCacheItemInvalidator_Tests() + { + _cache = GetRequiredService>(); + _tenantStore = GetRequiredService(); + _tenantRepository = GetRequiredService(); + } + + [Fact] + public async Task Get_Tenant_Should_Cached() + { + var acme = await _tenantRepository.FindByNameAsync("acme"); + acme.ShouldNotBeNull(); + + (await _cache.GetAsync(TenantCacheItem.CalculateCacheKey(acme.Id, null))).ShouldBeNull(); + (await _cache.GetAsync(TenantCacheItem.CalculateCacheKey(null, acme.Name))).ShouldBeNull(); + + await _tenantStore.FindAsync(acme.Id); + (await _cache.GetAsync(TenantCacheItem.CalculateCacheKey(acme.Id, null))).ShouldNotBeNull(); + + await _tenantStore.FindAsync(acme.Name); + (await _cache.GetAsync(TenantCacheItem.CalculateCacheKey(null, acme.Name))).ShouldNotBeNull(); + + + var volosoft = _tenantRepository.FindByName("volosoft"); + volosoft.ShouldNotBeNull(); + + (_cache.Get(TenantCacheItem.CalculateCacheKey(volosoft.Id, null))).ShouldBeNull(); + (_cache.Get(TenantCacheItem.CalculateCacheKey(null, volosoft.Name))).ShouldBeNull(); + + _tenantStore.Find(volosoft.Id); + (_cache.Get(TenantCacheItem.CalculateCacheKey(volosoft.Id, null))).ShouldNotBeNull(); + + _tenantStore.Find(volosoft.Name); + (_cache.Get(TenantCacheItem.CalculateCacheKey(null, volosoft.Name))).ShouldNotBeNull(); + } + + [Fact] + public async Task Cache_Should_Invalidator_When_Tenant_Changed() + { + var acme = await _tenantRepository.FindByNameAsync("acme"); + acme.ShouldNotBeNull(); + + // FindAsync will cache tenant. + await _tenantStore.FindAsync(acme.Id); + await _tenantStore.FindAsync(acme.Name); + + (await _cache.GetAsync(TenantCacheItem.CalculateCacheKey(acme.Id, null))).ShouldNotBeNull(); + (await _cache.GetAsync(TenantCacheItem.CalculateCacheKey(null, acme.Name))).ShouldNotBeNull(); + + await _tenantRepository.DeleteAsync(acme); + + (await _cache.GetAsync(TenantCacheItem.CalculateCacheKey(acme.Id, null))).ShouldBeNull(); + (await _cache.GetAsync(TenantCacheItem.CalculateCacheKey(null, acme.Name))).ShouldBeNull(); + + + var volosoft = await _tenantRepository.FindByNameAsync("volosoft"); + volosoft.ShouldNotBeNull(); + + // Find will cache tenant. + _tenantStore.Find(volosoft.Id); + _tenantStore.Find(volosoft.Name); + + (_cache.Get(TenantCacheItem.CalculateCacheKey(volosoft.Id, null))).ShouldNotBeNull(); + (_cache.Get(TenantCacheItem.CalculateCacheKey(null, volosoft.Name))).ShouldNotBeNull(); + + await _tenantRepository.DeleteAsync(volosoft); + + (_cache.Get(TenantCacheItem.CalculateCacheKey(volosoft.Id, null))).ShouldBeNull(); + (_cache.Get(TenantCacheItem.CalculateCacheKey(null, volosoft.Name))).ShouldBeNull(); + } + } +}