Browse Source

Add `NormalizedName` property to `Tenant`.

Resolve #18394
pull/18426/head
maliming 2 years ago
parent
commit
712d547c4b
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 14
      framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor/Volo/Abp/AspNetCore/Components/MauiBlazor/MauiBlazorRemoteTenantStore.cs
  2. 12
      framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/MvcRemoteTenantStore.cs
  3. 2
      framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/MultiTenancy/FindTenantResultDto.cs
  4. 8
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Pages/Abp/MultiTenancy/AbpTenantAppService.cs
  5. 5
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Pages/Abp/MultiTenancy/TenantSwitchModal.cshtml.cs
  6. 6
      framework/src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/ITenantNormalizer.cs
  7. 4
      framework/src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/ITenantStore.cs
  8. 10
      framework/src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/TenantConfiguration.cs
  9. 11
      framework/src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/UpperInvariantTenantNormalizer.cs
  10. 8
      framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/ConfigurationStore/DefaultTenantStore.cs
  11. 7
      framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/TenantConfigurationProvider.cs
  12. 3
      framework/test/Volo.Abp.AspNetCore.MultiTenancy.Tests/Volo/Abp/AspNetCore/MultiTenancy/AspNetCoreMultiTenancy_WithDomainResolver_Tests.cs
  13. 3
      framework/test/Volo.Abp.AspNetCore.MultiTenancy.Tests/Volo/Abp/AspNetCore/MultiTenancy/AspNetCoreMultiTenancy_Without_DomainResolver_Tests.cs
  14. 3
      framework/test/Volo.Abp.AspNetCore.Serilog.Tests/Volo/Abp/AspNetCore/Serilog/Serilog_Enrichers_Tests.cs
  15. 4
      modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo/Abp/TenantManagement/ITenantRepository.cs
  16. 12
      modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo/Abp/TenantManagement/Tenant.cs
  17. 8
      modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo/Abp/TenantManagement/TenantConfigurationCacheItemInvalidator.cs
  18. 24
      modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo/Abp/TenantManagement/TenantManager.cs
  19. 28
      modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo/Abp/TenantManagement/TenantStore.cs
  20. 1
      modules/tenant-management/src/Volo.Abp.TenantManagement.EntityFrameworkCore/Volo/Abp/TenantManagement/EntityFrameworkCore/AbpTenantManagementDbContextModelCreatingExtensions.cs
  21. 8
      modules/tenant-management/src/Volo.Abp.TenantManagement.EntityFrameworkCore/Volo/Abp/TenantManagement/EntityFrameworkCore/EfCoreTenantRepository.cs
  22. 8
      modules/tenant-management/src/Volo.Abp.TenantManagement.MongoDB/Volo/Abp/TenantManagement/MongoDb/MongoTenantRepository.cs
  23. 44
      modules/tenant-management/test/Volo.Abp.TenantManagement.Domain.Tests/Volo/Abp/TenantManagement/TenantCacheItemInvalidator_Tests.cs
  24. 11
      modules/tenant-management/test/Volo.Abp.TenantManagement.Domain.Tests/Volo/Abp/TenantManagement/TenantManager_Tests.cs
  25. 7
      modules/tenant-management/test/Volo.Abp.TenantManagement.Domain.Tests/Volo/Abp/TenantManagement/TenantStore_Tests.cs
  26. 7
      modules/tenant-management/test/Volo.Abp.TenantManagement.Domain.Tests/Volo/Abp/TenantManagement/Tenant_Tests.cs
  27. 5
      modules/tenant-management/test/Volo.Abp.TenantManagement.TestBase/Volo/Abp/TenantManagement/LazyLoad_Tests.cs
  28. 17
      modules/tenant-management/test/Volo.Abp.TenantManagement.TestBase/Volo/Abp/TenantManagement/TenantRepository_Tests.cs

14
framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor/Volo/Abp/AspNetCore/Components/MauiBlazor/MauiBlazorRemoteTenantStore.cs

@ -23,13 +23,13 @@ public class MauiBlazorRemoteTenantStore : ITenantStore, ITransientDependency
Cache = cache;
}
public async Task<TenantConfiguration?> FindAsync(string name)
public async Task<TenantConfiguration?> FindAsync(string normalizedName)
{
var cacheKey = CreateCacheKey(name);
var cacheKey = CreateCacheKey(normalizedName);
var tenantConfiguration = await Cache.GetOrAddAsync(
cacheKey,
async () => CreateTenantConfiguration(await TenantAppService.FindTenantByNameAsync(name))!,
async () => CreateTenantConfiguration(await TenantAppService.FindTenantByNameAsync(normalizedName))!,
() => new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow =
@ -57,13 +57,13 @@ public class MauiBlazorRemoteTenantStore : ITenantStore, ITransientDependency
return tenantConfiguration;
}
public TenantConfiguration? Find(string name)
public TenantConfiguration? Find(string normalizedName)
{
var cacheKey = CreateCacheKey(name);
var cacheKey = CreateCacheKey(normalizedName);
var tenantConfiguration = Cache.GetOrAdd(
cacheKey,
() => AsyncHelper.RunSync(async () => CreateTenantConfiguration(await TenantAppService.FindTenantByNameAsync(name)))!,
() => AsyncHelper.RunSync(async () => CreateTenantConfiguration(await TenantAppService.FindTenantByNameAsync(normalizedName)))!,
() => new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow =
@ -110,4 +110,4 @@ public class MauiBlazorRemoteTenantStore : ITenantStore, ITransientDependency
{
return $"RemoteTenantStore_Id_{tenantId:N}";
}
}
}

12
framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/MvcRemoteTenantStore.cs

@ -29,9 +29,9 @@ public class MvcRemoteTenantStore : ITenantStore, ITransientDependency
Options = options.Value;
}
public async Task<TenantConfiguration?> FindAsync(string name)
public async Task<TenantConfiguration?> FindAsync(string normalizedName)
{
var cacheKey = TenantConfigurationCacheItem.CalculateCacheKey(name);
var cacheKey = TenantConfigurationCacheItem.CalculateCacheKey(normalizedName);
var httpContext = HttpContextAccessor?.HttpContext;
if (httpContext != null && httpContext.Items[cacheKey] is TenantConfigurationCacheItem tenantConfigurationInHttpContext)
@ -42,7 +42,7 @@ public class MvcRemoteTenantStore : ITenantStore, ITransientDependency
var tenantConfiguration = await Cache.GetAsync(cacheKey);
if (tenantConfiguration == null)
{
await TenantAppService.FindTenantByNameAsync(name);
await TenantAppService.FindTenantByNameAsync(normalizedName);
tenantConfiguration = await Cache.GetAsync(cacheKey);
}
@ -79,9 +79,9 @@ public class MvcRemoteTenantStore : ITenantStore, ITransientDependency
return tenantConfiguration?.Value;
}
public TenantConfiguration? Find(string name)
public TenantConfiguration? Find(string normalizedName)
{
var cacheKey = TenantConfigurationCacheItem.CalculateCacheKey(name);
var cacheKey = TenantConfigurationCacheItem.CalculateCacheKey(normalizedName);
var httpContext = HttpContextAccessor?.HttpContext;
if (httpContext != null && httpContext.Items[cacheKey] is TenantConfigurationCacheItem tenantConfigurationInHttpContext)
@ -92,7 +92,7 @@ public class MvcRemoteTenantStore : ITenantStore, ITransientDependency
var tenantConfiguration = Cache.Get(cacheKey);
if (tenantConfiguration == null)
{
AsyncHelper.RunSync(async () => await TenantAppService.FindTenantByNameAsync(name));
AsyncHelper.RunSync(async () => await TenantAppService.FindTenantByNameAsync(normalizedName));
tenantConfiguration = Cache.Get(cacheKey);
}

2
framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/MultiTenancy/FindTenantResultDto.cs

@ -11,5 +11,7 @@ public class FindTenantResultDto
public string? Name { get; set; }
public string? NormalizedName { get; set; }
public bool IsActive { get; set; }
}

8
framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Pages/Abp/MultiTenancy/AbpTenantAppService.cs

@ -9,15 +9,17 @@ namespace Pages.Abp.MultiTenancy;
public class AbpTenantAppService : ApplicationService, IAbpTenantAppService
{
protected ITenantStore TenantStore { get; }
protected ITenantNormalizer TenantNormalizer { get; }
public AbpTenantAppService(ITenantStore tenantStore)
public AbpTenantAppService(ITenantStore tenantStore, ITenantNormalizer tenantNormalizer)
{
TenantStore = tenantStore;
TenantNormalizer = tenantNormalizer;
}
public virtual async Task<FindTenantResultDto> FindTenantByNameAsync(string name)
{
var tenant = await TenantStore.FindAsync(name);
var tenant = await TenantStore.FindAsync(TenantNormalizer.NormalizeName(name)!);
if (tenant == null)
{
@ -29,6 +31,7 @@ public class AbpTenantAppService : ApplicationService, IAbpTenantAppService
Success = true,
TenantId = tenant.Id,
Name = tenant.Name,
NormalizedName = tenant.NormalizedName,
IsActive = tenant.IsActive
};
}
@ -47,6 +50,7 @@ public class AbpTenantAppService : ApplicationService, IAbpTenantAppService
Success = true,
TenantId = tenant.Id,
Name = tenant.Name,
NormalizedName = tenant.NormalizedName,
IsActive = tenant.IsActive
};
}

5
framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Pages/Abp/MultiTenancy/TenantSwitchModal.cshtml.cs

@ -18,13 +18,16 @@ public class TenantSwitchModalModel : AbpPageModel
public TenantInfoModel Input { get; set; } = default!;
protected ITenantStore TenantStore { get; }
protected ITenantNormalizer TenantNormalizer { get; }
protected AbpAspNetCoreMultiTenancyOptions Options { get; }
public TenantSwitchModalModel(
ITenantStore tenantStore,
ITenantNormalizer tenantNormalizer,
IOptions<AbpAspNetCoreMultiTenancyOptions> options)
{
TenantStore = tenantStore;
TenantNormalizer = tenantNormalizer;
Options = options.Value;
LocalizationResourceType = typeof(AbpUiMultiTenancyResource);
}
@ -45,7 +48,7 @@ public class TenantSwitchModalModel : AbpPageModel
Guid? tenantId = null;
if (!Input.Name.IsNullOrEmpty())
{
var tenant = await TenantStore.FindAsync(Input.Name!);
var tenant = await TenantStore.FindAsync(TenantNormalizer.NormalizeName(Input.Name!)!);
if (tenant == null)
{
throw new UserFriendlyException(L["GivenTenantIsNotExist", Input.Name!]);

6
framework/src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/ITenantNormalizer.cs

@ -0,0 +1,6 @@
namespace Volo.Abp.MultiTenancy;
public interface ITenantNormalizer
{
string? NormalizeName(string? name);
}

4
framework/src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/ITenantStore.cs

@ -5,12 +5,12 @@ namespace Volo.Abp.MultiTenancy;
public interface ITenantStore
{
Task<TenantConfiguration?> FindAsync(string name);
Task<TenantConfiguration?> FindAsync(string normalizedName);
Task<TenantConfiguration?> FindAsync(Guid id);
[Obsolete("Use FindAsync method.")]
TenantConfiguration? Find(string name);
TenantConfiguration? Find(string normalizedName);
[Obsolete("Use FindAsync method.")]
TenantConfiguration? Find(Guid id);

10
framework/src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/TenantConfiguration.cs

@ -11,6 +11,8 @@ public class TenantConfiguration
public string Name { get; set; } = default!;
public string NormalizedName { get; set; } = default!;
public ConnectionStrings? ConnectionStrings { get; set; }
public bool IsActive { get; set; }
@ -30,4 +32,12 @@ public class TenantConfiguration
ConnectionStrings = new ConnectionStrings();
}
public TenantConfiguration(Guid id, [NotNull] string name, [NotNull] string normalizedName)
: this(id, name)
{
Check.NotNull(normalizedName, nameof(normalizedName));
NormalizedName = normalizedName;
}
}

11
framework/src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/UpperInvariantTenantNormalizer.cs

@ -0,0 +1,11 @@
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.MultiTenancy;
public class UpperInvariantTenantNormalizer : ITenantNormalizer, ITransientDependency
{
public virtual string? NormalizeName(string? name)
{
return name?.Normalize().ToUpperInvariant();
}
}

8
framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/ConfigurationStore/DefaultTenantStore.cs

@ -16,9 +16,9 @@ public class DefaultTenantStore : ITenantStore, ITransientDependency
_options = options.CurrentValue;
}
public Task<TenantConfiguration?> FindAsync(string name)
public Task<TenantConfiguration?> FindAsync(string normalizedName)
{
return Task.FromResult(Find(name));
return Task.FromResult(Find(normalizedName));
}
public Task<TenantConfiguration?> FindAsync(Guid id)
@ -26,9 +26,9 @@ public class DefaultTenantStore : ITenantStore, ITransientDependency
return Task.FromResult(Find(id));
}
public TenantConfiguration? Find(string name)
public TenantConfiguration? Find(string normalizedName)
{
return _options.Tenants?.FirstOrDefault(t => t.Name == name);
return _options.Tenants?.FirstOrDefault(t => t.NormalizedName == normalizedName);
}
public TenantConfiguration? Find(Guid id)

7
framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/TenantConfigurationProvider.cs

@ -10,6 +10,7 @@ public class TenantConfigurationProvider : ITenantConfigurationProvider, ITransi
{
protected virtual ITenantResolver TenantResolver { get; }
protected virtual ITenantStore TenantStore { get; }
protected virtual ITenantNormalizer TenantNormalizer { get; }
protected virtual ITenantResolveResultAccessor TenantResolveResultAccessor { get; }
protected virtual IStringLocalizer<AbpMultiTenancyResource> StringLocalizer { get; }
@ -17,10 +18,12 @@ public class TenantConfigurationProvider : ITenantConfigurationProvider, ITransi
ITenantResolver tenantResolver,
ITenantStore tenantStore,
ITenantResolveResultAccessor tenantResolveResultAccessor,
IStringLocalizer<AbpMultiTenancyResource> stringLocalizer)
IStringLocalizer<AbpMultiTenancyResource> stringLocalizer,
ITenantNormalizer tenantNormalizer)
{
TenantResolver = tenantResolver;
TenantStore = tenantStore;
TenantNormalizer = tenantNormalizer;
TenantResolveResultAccessor = tenantResolveResultAccessor;
StringLocalizer = stringLocalizer;
}
@ -69,7 +72,7 @@ public class TenantConfigurationProvider : ITenantConfigurationProvider, ITransi
}
else
{
return await TenantStore.FindAsync(tenantIdOrName);
return await TenantStore.FindAsync(TenantNormalizer.NormalizeName(tenantIdOrName)!);
}
}
}

3
framework/test/Volo.Abp.AspNetCore.MultiTenancy.Tests/Volo/Abp/AspNetCore/MultiTenancy/AspNetCoreMultiTenancy_WithDomainResolver_Tests.cs

@ -16,6 +16,7 @@ public class AspNetCoreMultiTenancy_WithDomainResolver_Tests : AspNetCoreMultiTe
{
private readonly Guid _testTenantId = Guid.NewGuid();
private readonly string _testTenantName = "acme";
private readonly string _testTenantNormalizedName = "ACME";
private readonly AbpAspNetCoreMultiTenancyOptions _options;
@ -30,7 +31,7 @@ public class AspNetCoreMultiTenancy_WithDomainResolver_Tests : AspNetCoreMultiTe
{
options.Tenants = new[]
{
new TenantConfiguration(_testTenantId, _testTenantName)
new TenantConfiguration(_testTenantId, _testTenantName, _testTenantNormalizedName)
};
});

3
framework/test/Volo.Abp.AspNetCore.MultiTenancy.Tests/Volo/Abp/AspNetCore/MultiTenancy/AspNetCoreMultiTenancy_Without_DomainResolver_Tests.cs

@ -17,6 +17,7 @@ public class AspNetCoreMultiTenancy_Without_DomainResolver_Tests : AspNetCoreMul
{
private readonly Guid _testTenantId = Guid.NewGuid();
private readonly string _testTenantName = "acme";
private readonly string _testTenantNormalizedName = "ACME";
private readonly AbpAspNetCoreMultiTenancyOptions _options;
@ -31,7 +32,7 @@ public class AspNetCoreMultiTenancy_Without_DomainResolver_Tests : AspNetCoreMul
{
options.Tenants = new[]
{
new TenantConfiguration(_testTenantId, _testTenantName)
new TenantConfiguration(_testTenantId, _testTenantName, _testTenantNormalizedName)
};
});

3
framework/test/Volo.Abp.AspNetCore.Serilog.Tests/Volo/Abp/AspNetCore/Serilog/Serilog_Enrichers_Tests.cs

@ -20,6 +20,7 @@ public class Serilog_Enrichers_Tests : AbpSerilogTestBase
private readonly Guid _testTenantId = Guid.NewGuid();
private readonly string _testTenantName = "acme";
private readonly string _testTenantNormalizedName = "ACME";
private readonly AbpAspNetCoreMultiTenancyOptions _tenancyOptions;
private readonly AbpAspNetCoreSerilogOptions _serilogOptions;
@ -39,7 +40,7 @@ public class Serilog_Enrichers_Tests : AbpSerilogTestBase
{
options.Tenants = new[]
{
new TenantConfiguration(_testTenantId, _testTenantName)
new TenantConfiguration(_testTenantId, _testTenantName, _testTenantNormalizedName)
};
});
base.ConfigureServices(services);

4
modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo/Abp/TenantManagement/ITenantRepository.cs

@ -9,13 +9,13 @@ namespace Volo.Abp.TenantManagement;
public interface ITenantRepository : IBasicRepository<Tenant, Guid>
{
Task<Tenant> FindByNameAsync(
string name,
string normalizedName,
bool includeDetails = true,
CancellationToken cancellationToken = default);
[Obsolete("Use FindByNameAsync method.")]
Tenant FindByName(
string name,
string normalizedName,
bool includeDetails = true
);

12
modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo/Abp/TenantManagement/Tenant.cs

@ -10,7 +10,9 @@ namespace Volo.Abp.TenantManagement;
public class Tenant : FullAuditedAggregateRoot<Guid>, IHasEntityVersion
{
public virtual string Name { get; protected set; }
public virtual string NormalizedName { get; protected set; }
public virtual int EntityVersion { get; protected set; }
public virtual List<TenantConnectionString> ConnectionStrings { get; protected set; }
@ -20,10 +22,11 @@ public class Tenant : FullAuditedAggregateRoot<Guid>, IHasEntityVersion
}
protected internal Tenant(Guid id, [NotNull] string name)
protected internal Tenant(Guid id, [NotNull] string name, [CanBeNull] string normalizedTenantName)
: base(id)
{
SetName(name);
SetNormalizedTenantName(normalizedTenantName);
ConnectionStrings = new List<TenantConnectionString>();
}
@ -78,4 +81,9 @@ public class Tenant : FullAuditedAggregateRoot<Guid>, IHasEntityVersion
{
Name = Check.NotNullOrWhiteSpace(name, nameof(name), TenantConsts.MaxNameLength);
}
protected internal virtual void SetNormalizedTenantName([CanBeNull] string normalizedTenantName)
{
NormalizedName = normalizedTenantName;
}
}

8
modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo/Abp/TenantManagement/TenantConfigurationCacheItemInvalidator.cs

@ -22,21 +22,21 @@ public class TenantConfigurationCacheItemInvalidator :
public virtual async Task HandleEventAsync(EntityChangedEventData<Tenant> eventData)
{
await ClearCacheAsync(eventData.Entity.Id, eventData.Entity.Name);
await ClearCacheAsync(eventData.Entity.Id, eventData.Entity.NormalizedName);
}
public virtual async Task HandleEventAsync(EntityDeletedEventData<Tenant> eventData)
{
await ClearCacheAsync(eventData.Entity.Id, eventData.Entity.Name);
await ClearCacheAsync(eventData.Entity.Id, eventData.Entity.NormalizedName);
}
protected virtual async Task ClearCacheAsync(Guid? id, string name)
protected virtual async Task ClearCacheAsync(Guid? id, string normalizedName)
{
await Cache.RemoveManyAsync(
new[]
{
TenantConfigurationCacheItem.CalculateCacheKey(id, null),
TenantConfigurationCacheItem.CalculateCacheKey(null, name),
TenantConfigurationCacheItem.CalculateCacheKey(null, normalizedName),
});
}
}

24
modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo/Abp/TenantManagement/TenantManager.cs

@ -11,19 +11,22 @@ public class TenantManager : DomainService, ITenantManager
protected ITenantRepository TenantRepository { get; }
protected IDistributedCache<TenantConfigurationCacheItem> Cache { get; }
public TenantManager(ITenantRepository tenantRepository,
IDistributedCache<TenantConfigurationCacheItem> cache)
protected ITenantNormalizer TenantNormalizer { get; }
public TenantManager(ITenantRepository tenantRepository, IDistributedCache<TenantConfigurationCacheItem> cache, ITenantNormalizer tenantNormalizer)
{
TenantRepository = tenantRepository;
Cache = cache;
TenantNormalizer = tenantNormalizer;
}
public virtual async Task<Tenant> CreateAsync(string name)
{
Check.NotNull(name, nameof(name));
await ValidateNameAsync(name);
return new Tenant(GuidGenerator.Create(), name);
var normalizedTenantName = TenantNormalizer.NormalizeName(name);
await ValidateNameAsync(normalizedTenantName);
return new Tenant(GuidGenerator.Create(), name, normalizedTenantName);
}
public virtual async Task ChangeNameAsync(Tenant tenant, string name)
@ -31,17 +34,20 @@ public class TenantManager : DomainService, ITenantManager
Check.NotNull(tenant, nameof(tenant));
Check.NotNull(name, nameof(name));
await ValidateNameAsync(name, tenant.Id);
await Cache.RemoveAsync(TenantConfigurationCacheItem.CalculateCacheKey(tenant.Name));
var normalizedTenantName = TenantNormalizer.NormalizeName(name);
await ValidateNameAsync(normalizedTenantName, tenant.Id);
await Cache.RemoveAsync(TenantConfigurationCacheItem.CalculateCacheKey(tenant.NormalizedName));
tenant.SetName(name);
tenant.SetNormalizedTenantName(normalizedTenantName);
}
protected virtual async Task ValidateNameAsync(string name, Guid? expectedId = null)
protected virtual async Task ValidateNameAsync(string normalizeName, Guid? expectedId = null)
{
var tenant = await TenantRepository.FindByNameAsync(name);
var tenant = await TenantRepository.FindByNameAsync(normalizeName);
if (tenant != null && tenant.Id != expectedId)
{
throw new BusinessException("Volo.Abp.TenantManagement:DuplicateTenantName").WithData("Name", name);
throw new BusinessException("Volo.Abp.TenantManagement:DuplicateTenantName").WithData("Name", normalizeName);
}
}
}

28
modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo/Abp/TenantManagement/TenantStore.cs

@ -27,9 +27,9 @@ public class TenantStore : ITenantStore, ITransientDependency
Cache = cache;
}
public virtual async Task<TenantConfiguration> FindAsync(string name)
public virtual async Task<TenantConfiguration> FindAsync(string normalizedName)
{
return (await GetCacheItemAsync(null, name)).Value;
return (await GetCacheItemAsync(null, normalizedName)).Value;
}
public virtual async Task<TenantConfiguration> FindAsync(Guid id)
@ -38,9 +38,9 @@ public class TenantStore : ITenantStore, ITransientDependency
}
[Obsolete("Use FindAsync method.")]
public virtual TenantConfiguration Find(string name)
public virtual TenantConfiguration Find(string normalizedName)
{
return (GetCacheItem(null, name)).Value;
return (GetCacheItem(null, normalizedName)).Value;
}
[Obsolete("Use FindAsync method.")]
@ -49,9 +49,9 @@ public class TenantStore : ITenantStore, ITransientDependency
return (GetCacheItem(id, null)).Value;
}
protected virtual async Task<TenantConfigurationCacheItem> GetCacheItemAsync(Guid? id, string name)
protected virtual async Task<TenantConfigurationCacheItem> GetCacheItemAsync(Guid? id, string normalizedName)
{
var cacheKey = CalculateCacheKey(id, name);
var cacheKey = CalculateCacheKey(id, normalizedName);
var cacheItem = await Cache.GetAsync(cacheKey, considerUow: true);
if (cacheItem != null)
@ -68,11 +68,11 @@ public class TenantStore : ITenantStore, ITransientDependency
}
}
if (!name.IsNullOrWhiteSpace())
if (!normalizedName.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);
var tenant = await TenantRepository.FindByNameAsync(normalizedName);
return await SetCacheAsync(cacheKey, tenant);
}
}
@ -89,9 +89,9 @@ public class TenantStore : ITenantStore, ITransientDependency
}
[Obsolete("Use GetCacheItemAsync method.")]
protected virtual TenantConfigurationCacheItem GetCacheItem(Guid? id, string name)
protected virtual TenantConfigurationCacheItem GetCacheItem(Guid? id, string normalizedName)
{
var cacheKey = CalculateCacheKey(id, name);
var cacheKey = CalculateCacheKey(id, normalizedName);
var cacheItem = Cache.Get(cacheKey, considerUow: true);
if (cacheItem != null)
@ -108,11 +108,11 @@ public class TenantStore : ITenantStore, ITransientDependency
}
}
if (!name.IsNullOrWhiteSpace())
if (!normalizedName.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);
var tenant = TenantRepository.FindByName(normalizedName);
return SetCache(cacheKey, tenant);
}
}
@ -129,8 +129,8 @@ public class TenantStore : ITenantStore, ITransientDependency
return cacheItem;
}
protected virtual string CalculateCacheKey(Guid? id, string name)
protected virtual string CalculateCacheKey(Guid? id, string normalizedName)
{
return TenantConfigurationCacheItem.CalculateCacheKey(id, name);
return TenantConfigurationCacheItem.CalculateCacheKey(id, normalizedName);
}
}

1
modules/tenant-management/src/Volo.Abp.TenantManagement.EntityFrameworkCore/Volo/Abp/TenantManagement/EntityFrameworkCore/AbpTenantManagementDbContextModelCreatingExtensions.cs

@ -23,6 +23,7 @@ public static class AbpTenantManagementDbContextModelCreatingExtensions
b.ConfigureByConvention();
b.Property(t => t.Name).IsRequired().HasMaxLength(TenantConsts.MaxNameLength);
b.Property(t => t.NormalizedName).IsRequired().HasMaxLength(TenantConsts.MaxNameLength);
b.HasMany(u => u.ConnectionStrings).WithOne().HasForeignKey(uc => uc.TenantId).IsRequired();

8
modules/tenant-management/src/Volo.Abp.TenantManagement.EntityFrameworkCore/Volo/Abp/TenantManagement/EntityFrameworkCore/EfCoreTenantRepository.cs

@ -19,23 +19,23 @@ public class EfCoreTenantRepository : EfCoreRepository<ITenantManagementDbContex
}
public virtual async Task<Tenant> FindByNameAsync(
string name,
string normalizedName,
bool includeDetails = true,
CancellationToken cancellationToken = default)
{
return await (await GetDbSetAsync())
.IncludeDetails(includeDetails)
.OrderBy(t => t.Id)
.FirstOrDefaultAsync(t => t.Name == name, GetCancellationToken(cancellationToken));
.FirstOrDefaultAsync(t => t.NormalizedName == normalizedName, GetCancellationToken(cancellationToken));
}
[Obsolete("Use FindByNameAsync method.")]
public virtual Tenant FindByName(string name, bool includeDetails = true)
public virtual Tenant FindByName(string normalizedName, bool includeDetails = true)
{
return DbSet
.IncludeDetails(includeDetails)
.OrderBy(t => t.Id)
.FirstOrDefault(t => t.Name == name);
.FirstOrDefault(t => t.NormalizedName == normalizedName);
}
[Obsolete("Use FindAsync method.")]

8
modules/tenant-management/src/Volo.Abp.TenantManagement.MongoDB/Volo/Abp/TenantManagement/MongoDb/MongoTenantRepository.cs

@ -20,19 +20,19 @@ public class MongoTenantRepository : MongoDbRepository<ITenantManagementMongoDbC
}
public virtual async Task<Tenant> FindByNameAsync(
string name,
string normalizedName,
bool includeDetails = true,
CancellationToken cancellationToken = default)
{
return await (await GetMongoQueryableAsync(cancellationToken))
.FirstOrDefaultAsync(t => t.Name == name, GetCancellationToken(cancellationToken));
.FirstOrDefaultAsync(t => t.NormalizedName == normalizedName, GetCancellationToken(cancellationToken));
}
[Obsolete("Use FindByNameAsync method.")]
public virtual Tenant FindByName(string name, bool includeDetails = true)
public virtual Tenant FindByName(string normalizedName, bool includeDetails = true)
{
return GetMongoQueryable()
.FirstOrDefault(t => t.Name == name);
.FirstOrDefault(t => t.NormalizedName == normalizedName);
}
[Obsolete("Use FindAsync method.")]

44
modules/tenant-management/test/Volo.Abp.TenantManagement.Domain.Tests/Volo/Abp/TenantManagement/TenantCacheItemInvalidator_Tests.cs

@ -12,6 +12,7 @@ public class TenantConfigurationCacheItemInvalidator_Tests : AbpTenantManagement
private readonly ITenantStore _tenantStore;
private readonly ITenantRepository _tenantRepository;
private readonly ITenantManager _tenantManager;
private readonly ITenantNormalizer _tenantNormalizer;
public TenantConfigurationCacheItemInvalidator_Tests()
{
@ -19,86 +20,87 @@ public class TenantConfigurationCacheItemInvalidator_Tests : AbpTenantManagement
_tenantStore = GetRequiredService<ITenantStore>();
_tenantRepository = GetRequiredService<ITenantRepository>();
_tenantManager = GetRequiredService<ITenantManager>();
_tenantNormalizer = GetRequiredService<ITenantNormalizer>();
}
[Fact]
public async Task Get_Tenant_Should_Cached()
{
var acme = await _tenantRepository.FindByNameAsync("acme");
var acme = await _tenantRepository.FindByNameAsync(_tenantNormalizer.NormalizeName("acme"));
acme.ShouldNotBeNull();
(await _cache.GetAsync(TenantConfigurationCacheItem.CalculateCacheKey(acme.Id, null))).ShouldBeNull();
(await _cache.GetAsync(TenantConfigurationCacheItem.CalculateCacheKey(null, acme.Name))).ShouldBeNull();
(await _cache.GetAsync(TenantConfigurationCacheItem.CalculateCacheKey(null, acme.NormalizedName))).ShouldBeNull();
await _tenantStore.FindAsync(acme.Id);
(await _cache.GetAsync(TenantConfigurationCacheItem.CalculateCacheKey(acme.Id, null))).ShouldNotBeNull();
await _tenantStore.FindAsync(acme.Name);
(await _cache.GetAsync(TenantConfigurationCacheItem.CalculateCacheKey(null, acme.Name))).ShouldNotBeNull();
await _tenantStore.FindAsync(acme.NormalizedName);
(await _cache.GetAsync(TenantConfigurationCacheItem.CalculateCacheKey(null, acme.NormalizedName))).ShouldNotBeNull();
var volosoft = _tenantRepository.FindByName("volosoft");
var volosoft = _tenantRepository.FindByName(_tenantNormalizer.NormalizeName("volosoft"));
volosoft.ShouldNotBeNull();
(_cache.Get(TenantConfigurationCacheItem.CalculateCacheKey(volosoft.Id, null))).ShouldBeNull();
(_cache.Get(TenantConfigurationCacheItem.CalculateCacheKey(null, volosoft.Name))).ShouldBeNull();
(_cache.Get(TenantConfigurationCacheItem.CalculateCacheKey(null, volosoft.NormalizedName))).ShouldBeNull();
_tenantStore.Find(volosoft.Id);
(_cache.Get(TenantConfigurationCacheItem.CalculateCacheKey(volosoft.Id, null))).ShouldNotBeNull();
_tenantStore.Find(volosoft.Name);
(_cache.Get(TenantConfigurationCacheItem.CalculateCacheKey(null, volosoft.Name))).ShouldNotBeNull();
_tenantStore.Find(volosoft.NormalizedName);
(_cache.Get(TenantConfigurationCacheItem.CalculateCacheKey(null, volosoft.NormalizedName))).ShouldNotBeNull();
}
[Fact]
public async Task Cache_Should_Invalidator_When_Tenant_Changed()
{
var acme = await _tenantRepository.FindByNameAsync("acme");
var acme = await _tenantRepository.FindByNameAsync(_tenantNormalizer.NormalizeName("acme"));
acme.ShouldNotBeNull();
// FindAsync will cache tenant.
await _tenantStore.FindAsync(acme.Id);
await _tenantStore.FindAsync(acme.Name);
await _tenantStore.FindAsync(acme.NormalizedName);
(await _cache.GetAsync(TenantConfigurationCacheItem.CalculateCacheKey(acme.Id, null))).ShouldNotBeNull();
(await _cache.GetAsync(TenantConfigurationCacheItem.CalculateCacheKey(null, acme.Name))).ShouldNotBeNull();
(await _cache.GetAsync(TenantConfigurationCacheItem.CalculateCacheKey(null, acme.NormalizedName))).ShouldNotBeNull();
await _tenantRepository.DeleteAsync(acme);
(await _cache.GetAsync(TenantConfigurationCacheItem.CalculateCacheKey(acme.Id, null))).ShouldBeNull();
(await _cache.GetAsync(TenantConfigurationCacheItem.CalculateCacheKey(null, acme.Name))).ShouldBeNull();
(await _cache.GetAsync(TenantConfigurationCacheItem.CalculateCacheKey(null, acme.NormalizedName))).ShouldBeNull();
var volosoft = await _tenantRepository.FindByNameAsync("volosoft");
var volosoft = await _tenantRepository.FindByNameAsync(_tenantNormalizer.NormalizeName("volosoft"));
volosoft.ShouldNotBeNull();
// Find will cache tenant.
_tenantStore.Find(volosoft.Id);
_tenantStore.Find(volosoft.Name);
_tenantStore.Find(volosoft.NormalizedName);
(_cache.Get(TenantConfigurationCacheItem.CalculateCacheKey(volosoft.Id, null))).ShouldNotBeNull();
(_cache.Get(TenantConfigurationCacheItem.CalculateCacheKey(null, volosoft.Name))).ShouldNotBeNull();
(_cache.Get(TenantConfigurationCacheItem.CalculateCacheKey(null, volosoft.NormalizedName))).ShouldNotBeNull();
await _tenantRepository.DeleteAsync(volosoft);
(_cache.Get(TenantConfigurationCacheItem.CalculateCacheKey(volosoft.Id, null))).ShouldBeNull();
(_cache.Get(TenantConfigurationCacheItem.CalculateCacheKey(null, volosoft.Name))).ShouldBeNull();
(_cache.Get(TenantConfigurationCacheItem.CalculateCacheKey(null, volosoft.NormalizedName))).ShouldBeNull();
var abp = await _tenantRepository.FindByNameAsync("abp");
var abp = await _tenantRepository.FindByNameAsync(_tenantNormalizer.NormalizeName("abp"));
abp.ShouldNotBeNull();
// Find will cache tenant.
await _tenantStore.FindAsync(abp.Id);
await _tenantStore.FindAsync(abp.Name);
await _tenantStore.FindAsync(abp.NormalizedName);
(await _cache.GetAsync(TenantConfigurationCacheItem.CalculateCacheKey(abp.Id, null))).ShouldNotBeNull();
(await _cache.GetAsync(TenantConfigurationCacheItem.CalculateCacheKey(null, abp.Name))).ShouldNotBeNull();
(await _cache.GetAsync(TenantConfigurationCacheItem.CalculateCacheKey(null, abp.NormalizedName))).ShouldNotBeNull();
await _tenantManager.ChangeNameAsync(abp, "abp2");
await _tenantRepository.UpdateAsync(abp);
(await _cache.GetAsync(TenantConfigurationCacheItem.CalculateCacheKey(abp.Id, null))).ShouldBeNull();
(await _cache.GetAsync(TenantConfigurationCacheItem.CalculateCacheKey(null, "abp"))).ShouldBeNull();
(await _cache.GetAsync(TenantConfigurationCacheItem.CalculateCacheKey(null, "abp2"))).ShouldBeNull();
(await _cache.GetAsync(TenantConfigurationCacheItem.CalculateCacheKey(null, _tenantNormalizer.NormalizeName("abp")))).ShouldBeNull();
(await _cache.GetAsync(TenantConfigurationCacheItem.CalculateCacheKey(null, _tenantNormalizer.NormalizeName("abp2")))).ShouldBeNull();
}
}

11
modules/tenant-management/test/Volo.Abp.TenantManagement.Domain.Tests/Volo/Abp/TenantManagement/TenantManager_Tests.cs

@ -1,5 +1,6 @@
using System.Threading.Tasks;
using Shouldly;
using Volo.Abp.MultiTenancy;
using Xunit;
namespace Volo.Abp.TenantManagement;
@ -8,19 +9,21 @@ public class TenantManager_Tests : AbpTenantManagementDomainTestBase
{
private readonly ITenantManager _tenantManager;
private readonly ITenantRepository _tenantRepository;
private readonly ITenantNormalizer _tenantNormalizer;
public TenantManager_Tests()
{
_tenantManager = GetRequiredService<ITenantManager>();
_tenantRepository = GetRequiredService<ITenantRepository>();
_tenantNormalizer = GetRequiredService<ITenantNormalizer>();
}
[Fact]
public async Task CreateAsync()
{
var tenant = await _tenantManager.CreateAsync("Test");
tenant.Name.ShouldBe("Test");
tenant.NormalizedName.ShouldBe(_tenantNormalizer.NormalizeName("Test"));
}
[Fact]
@ -32,18 +35,20 @@ public class TenantManager_Tests : AbpTenantManagementDomainTestBase
[Fact]
public async Task ChangeNameAsync()
{
var tenant = await _tenantRepository.FindByNameAsync("volosoft");
var tenant = await _tenantRepository.FindByNameAsync(_tenantNormalizer.NormalizeName("volosoft"));
tenant.ShouldNotBeNull();
tenant.NormalizedName.ShouldBe(_tenantNormalizer.NormalizeName("volosoft"));
await _tenantManager.ChangeNameAsync(tenant, "newVolosoft");
tenant.Name.ShouldBe("newVolosoft");
tenant.NormalizedName.ShouldBe(_tenantNormalizer.NormalizeName("newVolosoft"));
}
[Fact]
public async Task ChangeName_Tenant_Name_Can_Not_Duplicate()
{
var tenant = await _tenantRepository.FindByNameAsync("acme");
var tenant = await _tenantRepository.FindByNameAsync(_tenantNormalizer.NormalizeName("acme"));
tenant.ShouldNotBeNull();
await Assert.ThrowsAsync<BusinessException>(async () => await _tenantManager.ChangeNameAsync(tenant, "volosoft"));

7
modules/tenant-management/test/Volo.Abp.TenantManagement.Domain.Tests/Volo/Abp/TenantManagement/TenantStore_Tests.cs

@ -9,25 +9,28 @@ public class TenantStore_Tests : AbpTenantManagementDomainTestBase
{
private readonly ITenantStore _tenantStore;
private readonly ITenantRepository _tenantRepository;
private readonly ITenantNormalizer _tenantNormalizer;
public TenantStore_Tests()
{
_tenantStore = GetRequiredService<ITenantStore>();
_tenantRepository = GetRequiredService<ITenantRepository>();
_tenantNormalizer = GetRequiredService<ITenantNormalizer>();
}
[Fact]
public async Task FindAsyncByName()
{
var acme = await _tenantStore.FindAsync("acme");
var acme = await _tenantStore.FindAsync(_tenantNormalizer.NormalizeName("acme")!);
acme.ShouldNotBeNull();
acme.Name.ShouldBe("acme");
acme.NormalizedName.ShouldBe(_tenantNormalizer.NormalizeName("acme")!);
}
[Fact]
public async Task FindAsyncById()
{
var acme = await _tenantRepository.FindByNameAsync("acme");
var acme = await _tenantRepository.FindByNameAsync(_tenantNormalizer.NormalizeName("acme"));
acme.ShouldNotBeNull();
(await _tenantStore.FindAsync(acme.Id)).ShouldNotBeNull();

7
modules/tenant-management/test/Volo.Abp.TenantManagement.Domain.Tests/Volo/Abp/TenantManagement/Tenant_Tests.cs

@ -1,5 +1,6 @@
using System.Threading.Tasks;
using Shouldly;
using Volo.Abp.MultiTenancy;
using Xunit;
namespace Volo.Abp.TenantManagement;
@ -7,16 +8,18 @@ namespace Volo.Abp.TenantManagement;
public class Tenant_Tests : AbpTenantManagementDomainTestBase
{
private readonly ITenantRepository _tenantRepository;
private readonly ITenantNormalizer _tenantNormalizer;
public Tenant_Tests()
{
_tenantRepository = GetRequiredService<ITenantRepository>();
_tenantNormalizer = GetRequiredService<ITenantNormalizer>();
}
[Fact]
public async Task FindDefaultConnectionString()
{
var acme = await _tenantRepository.FindByNameAsync("acme");
var acme = await _tenantRepository.FindByNameAsync(_tenantNormalizer.NormalizeName("acme"));
acme.ShouldNotBeNull();
acme.FindDefaultConnectionString().ShouldBe("DefaultConnString-Value");
@ -25,7 +28,7 @@ public class Tenant_Tests : AbpTenantManagementDomainTestBase
[Fact]
public async Task FindConnectionString()
{
var acme = await _tenantRepository.FindByNameAsync("acme");
var acme = await _tenantRepository.FindByNameAsync(_tenantNormalizer.NormalizeName("acme"));
acme.ShouldNotBeNull();
acme.FindConnectionString(Data.ConnectionStrings.DefaultConnectionStringName).ShouldBe("DefaultConnString-Value");

5
modules/tenant-management/test/Volo.Abp.TenantManagement.TestBase/Volo/Abp/TenantManagement/LazyLoad_Tests.cs

@ -2,6 +2,7 @@
using System.Threading.Tasks;
using Shouldly;
using Volo.Abp.Modularity;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Uow;
using Xunit;
@ -11,10 +12,12 @@ public abstract class LazyLoad_Tests<TStartupModule> : TenantManagementTestBase<
where TStartupModule : IAbpModule
{
public ITenantRepository TenantRepository { get; }
public ITenantNormalizer TenantNormalizer { get; }
protected LazyLoad_Tests()
{
TenantRepository = GetRequiredService<ITenantRepository>();
TenantNormalizer = GetRequiredService<ITenantNormalizer>();
}
[Fact]
@ -22,7 +25,7 @@ public abstract class LazyLoad_Tests<TStartupModule> : TenantManagementTestBase<
{
using (var uow = GetRequiredService<IUnitOfWorkManager>().Begin())
{
var role = await TenantRepository.FindByNameAsync("acme", includeDetails: false);
var role = await TenantRepository.FindByNameAsync(TenantNormalizer.NormalizeName("acme"), includeDetails: false);
role.ConnectionStrings.ShouldNotBeNull();
role.ConnectionStrings.Any().ShouldBeTrue();

17
modules/tenant-management/test/Volo.Abp.TenantManagement.TestBase/Volo/Abp/TenantManagement/TenantRepository_Tests.cs

@ -3,6 +3,7 @@ using System.Linq;
using System.Threading.Tasks;
using Shouldly;
using Volo.Abp.Modularity;
using Volo.Abp.MultiTenancy;
using Xunit;
namespace Volo.Abp.TenantManagement;
@ -11,22 +12,24 @@ public abstract class TenantRepository_Tests<TStartupModule> : TenantManagementT
where TStartupModule : IAbpModule
{
public ITenantRepository TenantRepository { get; }
public ITenantNormalizer TenantNormalizer { get; }
protected TenantRepository_Tests()
{
TenantRepository = GetRequiredService<ITenantRepository>();
TenantNormalizer = GetRequiredService<ITenantNormalizer>();
}
[Fact]
public async Task FindByNameAsync()
{
var tenant = await TenantRepository.FindByNameAsync("acme");
var tenant = await TenantRepository.FindByNameAsync(TenantNormalizer.NormalizeName("acme"));
tenant.ShouldNotBeNull();
tenant = await TenantRepository.FindByNameAsync("undefined-tenant");
tenant = await TenantRepository.FindByNameAsync(TenantNormalizer.NormalizeName("undefined-tenant"));
tenant.ShouldBeNull();
tenant = await TenantRepository.FindByNameAsync("acme", includeDetails: true);
tenant = await TenantRepository.FindByNameAsync(TenantNormalizer.NormalizeName("acme"), includeDetails: true);
tenant.ShouldNotBeNull();
tenant.ConnectionStrings.Count.ShouldBeGreaterThanOrEqualTo(2);
}
@ -34,7 +37,7 @@ public abstract class TenantRepository_Tests<TStartupModule> : TenantManagementT
[Fact]
public async Task FindAsync()
{
var tenantId = (await TenantRepository.FindByNameAsync("acme")).Id;
var tenantId = (await TenantRepository.FindByNameAsync(TenantNormalizer.NormalizeName("acme"))).Id;
var tenant = await TenantRepository.FindAsync(tenantId);
tenant.ShouldNotBeNull();
@ -51,14 +54,14 @@ public abstract class TenantRepository_Tests<TStartupModule> : TenantManagementT
public async Task GetListAsync()
{
var tenants = await TenantRepository.GetListAsync();
tenants.ShouldContain(t => t.Name == "acme");
tenants.ShouldContain(t => t.Name == "volosoft");
tenants.ShouldContain(t => t.Name == "acme" && t.NormalizedName == TenantNormalizer.NormalizeName("acme"));
tenants.ShouldContain(t => t.Name == "volosoft" && t.NormalizedName == TenantNormalizer.NormalizeName("volosoft"));
}
[Fact]
public async Task Should_Eager_Load_Tenant_Collections()
{
var role = await TenantRepository.FindByNameAsync("acme");
var role = await TenantRepository.FindByNameAsync(TenantNormalizer.NormalizeName("acme"));
role.ConnectionStrings.ShouldNotBeNull();
role.ConnectionStrings.Any().ShouldBeTrue();
}

Loading…
Cancel
Save