Created application layer and tests for multitenancy.

pull/206/head
Halil İbrahim Kalkan 8 years ago
parent d9bd003021
commit 6ac16a3405

@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27130.0
VisualStudioVersion = 15.0.27130.2026
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}"
EndProject
@ -228,11 +228,19 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{282A
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Abp.MultiTenancy", "Abp.MultiTenancy", "{CA154803-3589-47B3-B7CB-B18F94FE1EB6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.MultiTenancy.EntityFrameworkCore", "src\Volo.Abp.MultiTenancy.EntityFrameworkCore\Volo.Abp.MultiTenancy.EntityFrameworkCore.csproj", "{9D7C2C19-1EE4-40BB-A1A4-27B175F1BFDD}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.MultiTenancy.EntityFrameworkCore", "src\Volo.Abp.MultiTenancy.EntityFrameworkCore\Volo.Abp.MultiTenancy.EntityFrameworkCore.csproj", "{9D7C2C19-1EE4-40BB-A1A4-27B175F1BFDD}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.MultiTenancy.Domain", "src\Volo.Abp.MultiTenancy.Domain\Volo.Abp.MultiTenancy.Domain.csproj", "{3FA1F4A4-BF5A-4CE3-B76F-2147FE31AB43}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.MultiTenancy.Domain.Shared", "src\Volo.Abp.MultiTenancy.Domain.Shared\Volo.Abp.MultiTenancy.Domain.Shared.csproj", "{4605A64C-B4E0-4A04-9BF2-501027375258}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.MultiTenancy.Domain.Shared", "src\Volo.Abp.MultiTenancy.Domain.Shared\Volo.Abp.MultiTenancy.Domain.Shared.csproj", "{4605A64C-B4E0-4A04-9BF2-501027375258}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.MultiTenancy.Application.Contracts", "src\Volo.Abp.MultiTenancy.Application.Contracts\Volo.Abp.MultiTenancy.Application.Contracts.csproj", "{6A99C679-EA3B-4324-8D6B-89DB8C7D3DA0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.MultiTenancy.Application", "src\Volo.Abp.MultiTenancy.Application\Volo.Abp.MultiTenancy.Application.csproj", "{FCEC7C9D-067F-452B-98C9-1878F7EA02EB}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Abp.MultiTenancy", "Abp.MultiTenancy", "{3CE22CAC-4B27-4EEC-A35E-C01219ED6E99}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.MultiTenancy.Application.Tests", "test\Volo.Abp.MultiTenancy.Application.Tests\Volo.Abp.MultiTenancy.Application.Tests.csproj", "{B65973F1-9EE0-4914-B6F2-D88F753E1D21}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -608,6 +616,18 @@ Global
{4605A64C-B4E0-4A04-9BF2-501027375258}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4605A64C-B4E0-4A04-9BF2-501027375258}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4605A64C-B4E0-4A04-9BF2-501027375258}.Release|Any CPU.Build.0 = Release|Any CPU
{6A99C679-EA3B-4324-8D6B-89DB8C7D3DA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6A99C679-EA3B-4324-8D6B-89DB8C7D3DA0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6A99C679-EA3B-4324-8D6B-89DB8C7D3DA0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6A99C679-EA3B-4324-8D6B-89DB8C7D3DA0}.Release|Any CPU.Build.0 = Release|Any CPU
{FCEC7C9D-067F-452B-98C9-1878F7EA02EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FCEC7C9D-067F-452B-98C9-1878F7EA02EB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FCEC7C9D-067F-452B-98C9-1878F7EA02EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FCEC7C9D-067F-452B-98C9-1878F7EA02EB}.Release|Any CPU.Build.0 = Release|Any CPU
{B65973F1-9EE0-4914-B6F2-D88F753E1D21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B65973F1-9EE0-4914-B6F2-D88F753E1D21}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B65973F1-9EE0-4914-B6F2-D88F753E1D21}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B65973F1-9EE0-4914-B6F2-D88F753E1D21}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -721,6 +741,10 @@ Global
{9D7C2C19-1EE4-40BB-A1A4-27B175F1BFDD} = {CA154803-3589-47B3-B7CB-B18F94FE1EB6}
{3FA1F4A4-BF5A-4CE3-B76F-2147FE31AB43} = {CA154803-3589-47B3-B7CB-B18F94FE1EB6}
{4605A64C-B4E0-4A04-9BF2-501027375258} = {CA154803-3589-47B3-B7CB-B18F94FE1EB6}
{6A99C679-EA3B-4324-8D6B-89DB8C7D3DA0} = {CA154803-3589-47B3-B7CB-B18F94FE1EB6}
{FCEC7C9D-067F-452B-98C9-1878F7EA02EB} = {CA154803-3589-47B3-B7CB-B18F94FE1EB6}
{3CE22CAC-4B27-4EEC-A35E-C01219ED6E99} = {447C8A77-E5F0-4538-8687-7383196D04EA}
{B65973F1-9EE0-4914-B6F2-D88F753E1D21} = {3CE22CAC-4B27-4EEC-A35E-C01219ED6E99}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BB97ECF4-9A84-433F-A80B-2A3285BDD1D5}

@ -26,9 +26,9 @@ namespace AbpDesk.EntityFrameworkCore
{
base.OnModelCreating(modelBuilder);
this.ConfigureMultiTenancy(modelBuilder);
this.ConfigureMultiTenancy(modelBuilder, MultiTenancyDbContext.TablePrefix, MultiTenancyDbContext.Schema);
//Use different classes to map each entity type?
//Use different classes to map each entity type, as a better practice?
modelBuilder.Entity<Ticket>(b =>
{
b.ToTable("DskTickets");

@ -1,13 +0,0 @@
namespace Volo.Abp.Application.Dtos
{
/// <summary>
/// This interface is defined to standardize to set "Total Count of Items" to a DTO for long type.
/// </summary>
public interface IHasLongTotalCount
{
/// <summary>
/// Total count of Items.
/// </summary>
long TotalCount { get; set; }
}
}

@ -8,6 +8,6 @@ namespace Volo.Abp.Application.Dtos
/// <summary>
/// Total count of Items.
/// </summary>
int TotalCount { get; set; }
long TotalCount { get; set; }
}
}

@ -11,7 +11,7 @@ namespace Volo.Abp.Application.Dtos
public class PagedResultDto<T> : ListResultDto<T>, IPagedResult<T>
{
/// <inheritdoc />
public int TotalCount { get; set; }
public long TotalCount { get; set; } //TODO: Can be a long value..?
/// <summary>
/// Creates a new <see cref="PagedResultDto{T}"/> object.
@ -26,7 +26,7 @@ namespace Volo.Abp.Application.Dtos
/// </summary>
/// <param name="totalCount">Total count of Items</param>
/// <param name="items">List of items in current page</param>
public PagedResultDto(int totalCount, IReadOnlyList<T> items)
public PagedResultDto(long totalCount, IReadOnlyList<T> items)
: base(items)
{
TotalCount = totalCount;

@ -1,6 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.AutoMapper;
using Volo.Abp.Identity.ObjectMappings;
using Volo.Abp.Modularity;
namespace Volo.Abp.Identity

@ -1,6 +1,6 @@
using AutoMapper;
namespace Volo.Abp.Identity.ObjectMappings
namespace Volo.Abp.Identity
{
public class AbpIdentityApplicationModuleAutoMapperProfile : Profile
{

@ -27,7 +27,7 @@ namespace Volo.Abp.Identity
public async Task<PagedResultDto<IdentityUserDto>> GetListAsync(GetIdentityUsersInput input)
{
var count = (int)await _userRepository.GetCountAsync();
var count = await _userRepository.GetCountAsync();
var list = await _userRepository.GetListAsync(input.Sorting, input.MaxResultCount, input.SkipCount, input.Filter);
return new PagedResultDto<IdentityUserDto>(

@ -90,7 +90,8 @@ namespace Volo.Abp.Identity
u.Email.Contains(filter)
)
.OrderBy(sorting ?? nameof(IdentityUser.UserName))
.PageBy(skipCount, maxResultCount).ToListAsync(cancellationToken);
.PageBy(skipCount, maxResultCount)
.ToListAsync(cancellationToken);
}
public async Task<List<IdentityUser>> GetListAsync(string sorting, int maxResultCount, int skipCount)

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\common.props" />
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<AssemblyName>Volo.Abp.MultiTenancy.Application.Contracts</AssemblyName>
<PackageId>Volo.Abp.MultiTenancy.Application.Contracts</PackageId>
<AssetTargetFallback>$(AssetTargetFallback);portable-net45+win8+wp8+wpa81;</AssetTargetFallback>
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Volo.Abp.Ddd\Volo.Abp.Ddd.csproj" />
<ProjectReference Include="..\Volo.Abp.MultiTenancy.Domain.Shared\Volo.Abp.MultiTenancy.Domain.Shared.csproj" />
</ItemGroup>
</Project>

@ -0,0 +1,15 @@
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Modularity;
namespace Volo.Abp.MultiTenancy
{
[DependsOn(typeof(AbpDddModule))]
[DependsOn(typeof(AbpMultiTenancyDomainSharedModule))]
public class AbpMultiTenancyApplicationContractsModule : AbpModule
{
public override void ConfigureServices(IServiceCollection services)
{
services.AddAssemblyOf<AbpMultiTenancyApplicationContractsModule>();
}
}
}

@ -0,0 +1,9 @@
using Volo.Abp.Application.Dtos;
namespace Volo.Abp.MultiTenancy
{
public class GetTenantsInput : PagedAndSortedResultRequestDto
{
public string Filter { get; set; }
}
}

@ -0,0 +1,10 @@
using System;
using Volo.Abp.Application.Services;
namespace Volo.Abp.MultiTenancy
{
public interface ITenantAppService : IAsyncCrudAppService<TenantDto, Guid, GetTenantsInput, TenantCreateDto, TenantUpdateDto>
{
//TODO: Manage connection strings
}
}

@ -0,0 +1,7 @@
namespace Volo.Abp.MultiTenancy
{
public class TenantCreateDto : TenantCreateOrUpdateDtoBase
{
}
}

@ -0,0 +1,7 @@
namespace Volo.Abp.MultiTenancy
{
public abstract class TenantCreateOrUpdateDtoBase
{
public string Name { get; set; }
}
}

@ -0,0 +1,10 @@
using System;
using Volo.Abp.Application.Dtos;
namespace Volo.Abp.MultiTenancy
{
public class TenantDto : EntityDto<Guid>
{
public string Name { get; set; }
}
}

@ -0,0 +1,7 @@
namespace Volo.Abp.MultiTenancy
{
public class TenantUpdateDto : TenantCreateOrUpdateDtoBase
{
}
}

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\common.props" />
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<AssemblyName>Volo.Abp.MultiTenancy.Application</AssemblyName>
<PackageId>Volo.Abp.MultiTenancy.Application</PackageId>
<AssetTargetFallback>$(AssetTargetFallback);portable-net45+win8+wp8+wpa81;</AssetTargetFallback>
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Volo.Abp.MultiTenancy.Application.Contracts\Volo.Abp.MultiTenancy.Application.Contracts.csproj" />
<ProjectReference Include="..\Volo.Abp.MultiTenancy.Domain\Volo.Abp.MultiTenancy.Domain.csproj" />
<ProjectReference Include="..\Volo.Abp.UI\Volo.Abp.UI.csproj" />
</ItemGroup>
</Project>

@ -0,0 +1,21 @@
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.AutoMapper;
using Volo.Abp.Modularity;
namespace Volo.Abp.MultiTenancy
{
[DependsOn(typeof(AbpMultiTenancyDomainModule))]
[DependsOn(typeof(AbpMultiTenancyApplicationContractsModule))]
public class AbpMultiTenancyApplicationModule : AbpModule
{
public override void ConfigureServices(IServiceCollection services)
{
services.Configure<AbpAutoMapperOptions>(options =>
{
options.AddProfile<AbpMultiTenancyApplicationModuleAutoMapperProfile>(validate: true);
});
services.AddAssemblyOf<AbpMultiTenancyApplicationModule>();
}
}
}

@ -0,0 +1,12 @@
using AutoMapper;
namespace Volo.Abp.MultiTenancy
{
public class AbpMultiTenancyApplicationModuleAutoMapperProfile : Profile
{
public AbpMultiTenancyApplicationModuleAutoMapperProfile()
{
CreateMap<Tenant, TenantDto>();
}
}
}

@ -0,0 +1,9 @@
using Volo.Abp.Application.Services;
namespace Volo.Abp.MultiTenancy
{
public class MultiTenancyAppServiceBase : ApplicationService
{
}
}

@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Application.Dtos;
namespace Volo.Abp.MultiTenancy
{
public class TenantAppService : MultiTenancyAppServiceBase, ITenantAppService
{
private readonly ITenantRepository _tenantRepository;
private readonly ITenantManager _tenantManager;
public TenantAppService(ITenantRepository tenantRepository, ITenantManager tenantManager)
{
_tenantRepository = tenantRepository;
_tenantManager = tenantManager;
}
public async Task<TenantDto> GetAsync(Guid id)
{
return ObjectMapper.Map<Tenant, TenantDto>(
await _tenantRepository.GetAsync(id)
);
}
public async Task<PagedResultDto<TenantDto>> GetListAsync(GetTenantsInput input)
{
var count = await _tenantRepository.GetCountAsync();
var list = await _tenantRepository.GetListAsync(input.Sorting, input.MaxResultCount, input.SkipCount, input.Filter);
return new PagedResultDto<TenantDto>(
count,
ObjectMapper.Map<List<Tenant>, List<TenantDto>>(list)
);
}
public async Task<TenantDto> CreateAsync(TenantCreateDto input)
{
var tenant = await _tenantManager.CreateAsync(input.Name);
await _tenantRepository.InsertAsync(tenant);
return ObjectMapper.Map<Tenant, TenantDto>(tenant);
}
public async Task<TenantDto> UpdateAsync(Guid id, TenantUpdateDto input)
{
var tenant = await _tenantRepository.GetAsync(id);
await _tenantManager.ChangeNameAsync(tenant, input.Name);
await _tenantRepository.UpdateAsync(tenant);
return ObjectMapper.Map<Tenant, TenantDto>(tenant);
}
public async Task DeleteAsync(Guid id)
{
var tenant = await _tenantRepository.FindAsync(id);
if (tenant == null)
{
return;
}
await _tenantRepository.DeleteAsync(tenant);
}
}
}

@ -2,7 +2,7 @@
{
public static class TenantConnectionStringConsts
{
public const int MaxNameLength = 256;
public const int MaxNameLength = 128;
public const int MaxValueLength = 1024;
}

@ -19,6 +19,7 @@
<ProjectReference Include="..\Volo.Abp.Ddd\Volo.Abp.Ddd.csproj" />
<ProjectReference Include="..\Volo.Abp.MultiTenancy.Abstractions\Volo.Abp.MultiTenancy.Abstractions.csproj" />
<ProjectReference Include="..\Volo.Abp.MultiTenancy.Domain.Shared\Volo.Abp.MultiTenancy.Domain.Shared.csproj" />
<ProjectReference Include="..\Volo.Abp.UI\Volo.Abp.UI.csproj" />
</ItemGroup>
</Project>

@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/CSharpLanguageProject/LanguageLevel/@EntryValue">CSharp71</s:String></wpf:ResourceDictionary>

@ -2,6 +2,7 @@
using Volo.Abp.AutoMapper;
using Volo.Abp.Data;
using Volo.Abp.Modularity;
using Volo.Abp.Ui;
namespace Volo.Abp.MultiTenancy
{
@ -10,6 +11,7 @@ namespace Volo.Abp.MultiTenancy
[DependsOn(typeof(AbpDataModule))]
[DependsOn(typeof(AbpDddModule))]
[DependsOn(typeof(AbpAutoMapperModule))]
[DependsOn(typeof(AbpUiModule))] //TODO: It's not good to depend on the UI module. However, UserFriendlyException is inside it!
public class AbpMultiTenancyDomainModule : AbpModule
{
public override void ConfigureServices(IServiceCollection services)

@ -0,0 +1,14 @@
using System.Threading.Tasks;
using JetBrains.Annotations;
using Volo.Abp.Domain.Services;
namespace Volo.Abp.MultiTenancy
{
public interface ITenantManager : IDomainService
{
[NotNull]
Task<Tenant> CreateAsync([NotNull] string name);
Task ChangeNameAsync([NotNull] Tenant tenant, [NotNull] string name);
}
}

@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Volo.Abp.Domain.Repositories;
@ -6,8 +8,14 @@ namespace Volo.Abp.MultiTenancy
{
public interface ITenantRepository : IBasicRepository<Tenant, Guid>
{
Task<Tenant> FindByNameIncludeDetailsAsync(string name);
Task<Tenant> FindByNameAsync(string name, CancellationToken cancellationToken = default);
Task<Tenant> FindWithIncludeDetailsAsync(Guid id);
Task<Tenant> FindByNameIncludeDetailsAsync(string name, CancellationToken cancellationToken = default);
Task<Tenant> FindWithIncludeDetailsAsync(Guid id, CancellationToken cancellationToken = default);
Task<long> GetCountAsync(CancellationToken cancellationToken = default);
Task<List<Tenant>> GetListAsync(string sorting, int maxResultCount, int skipCount, string filter, CancellationToken cancellationToken = default);
}
}

@ -17,8 +17,10 @@ namespace Volo.Abp.MultiTenancy
}
public Tenant(Guid id, [NotNull] string name)
protected internal Tenant(Guid id, [NotNull] string name)
{
Check.NotNull(name, nameof(name));
Id = id;
Name = name;
@ -36,5 +38,12 @@ namespace Volo.Abp.MultiTenancy
{
return ConnectionStrings.FirstOrDefault(c => c.Name == name)?.Value;
}
internal void SetName([NotNull] string name)
{
Check.NotNull(name, nameof(name));
Name = name;
}
}
}

@ -4,7 +4,7 @@ using Volo.Abp.Domain.Entities;
namespace Volo.Abp.MultiTenancy
{
public class TenantConnectionString : Entity<Guid> //TODO: PK should be TenantId + Name (so, inherit from Entity)
public class TenantConnectionString : Entity
{
public virtual Guid TenantId { get; protected set; }

@ -0,0 +1,43 @@
using System;
using System.Threading.Tasks;
using Volo.Abp.Domain.Services;
using Volo.Abp.Ui;
namespace Volo.Abp.MultiTenancy
{
public class TenantManager : DomainService, ITenantManager
{
private readonly ITenantRepository _tenantRepository;
public TenantManager(ITenantRepository tenantRepository)
{
_tenantRepository = tenantRepository;
}
public async Task<Tenant> CreateAsync(string name)
{
Check.NotNull(name, nameof(name));
await ValidateNameAsync(name);
return new Tenant(GuidGenerator.Create(), name);
}
public async Task ChangeNameAsync(Tenant tenant, string name)
{
Check.NotNull(tenant, nameof(tenant));
Check.NotNull(name, nameof(name));
await ValidateNameAsync(name, tenant.Id);
tenant.SetName(name);
}
protected virtual async Task ValidateNameAsync(string name, Guid? expectedId = null)
{
var tenant = await _tenantRepository.FindByNameAsync(name);
if (tenant != null && tenant.Id != expectedId)
{
throw new UserFriendlyException("Duplicate tenancy name: " + name); //TODO: A domain exception would be better..?
}
}
}
}

@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/CSharpLanguageProject/LanguageLevel/@EntryValue">CSharp71</s:String></wpf:ResourceDictionary>

@ -1,4 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Dynamic.Core;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Volo.Abp.Domain.Repositories.EntityFrameworkCore;
@ -12,20 +16,44 @@ namespace Volo.Abp.MultiTenancy
public EfCoreTenantRepository(IDbContextProvider<IMultiTenancyDbContext> dbContextProvider)
: base(dbContextProvider)
{
}
public Task<Tenant> FindByNameIncludeDetailsAsync(string name)
public async Task<Tenant> FindByNameAsync(string name, CancellationToken cancellationToken = default)
{
return DbSet
.Include(t => t.ConnectionStrings) //TODO: Why not creating a virtual Include method in EfCoreRepository and override to add included properties to be available for every query..?
.FirstOrDefaultAsync(t => t.Name == name);
return await DbSet
.FirstOrDefaultAsync(t => t.Name == name, cancellationToken);
}
public Task<Tenant> FindWithIncludeDetailsAsync(Guid id)
public async Task<Tenant> FindByNameIncludeDetailsAsync(string name, CancellationToken cancellationToken = default)
{
return DbSet
return await DbSet
.Include(t => t.ConnectionStrings) //TODO: Why not creating a virtual Include method in EfCoreRepository and override to add included properties to be available for every query..?
.FirstOrDefaultAsync(t => t.Id == id);
.FirstOrDefaultAsync(t => t.Name == name, cancellationToken);
}
public async Task<Tenant> FindWithIncludeDetailsAsync(Guid id, CancellationToken cancellationToken = default)
{
return await DbSet
.Include(t => t.ConnectionStrings)
.FirstOrDefaultAsync(t => t.Id == id, cancellationToken);
}
public async Task<long> GetCountAsync(CancellationToken cancellationToken = default)
{
return await DbSet.LongCountAsync(cancellationToken);
}
public async Task<List<Tenant>> GetListAsync(string sorting, int maxResultCount, int skipCount, string filter, CancellationToken cancellationToken = default)
{
return await this.WhereIf(
!filter.IsNullOrWhiteSpace(),
u =>
u.Name.Contains(filter)
)
.OrderBy(sorting ?? nameof(Tenant.Name))
.PageBy(skipCount, maxResultCount)
.ToListAsync(cancellationToken);
}
}
}

@ -5,6 +5,10 @@ namespace Volo.Abp.MultiTenancy.EntityFrameworkCore
{
public class MultiTenancyDbContext : AbpDbContext<MultiTenancyDbContext>, IMultiTenancyDbContext
{
public static string TablePrefix { get; set; } = "Mt";
public static string Schema { get; set; }
public DbSet<Tenant> Tenants { get; set; }
public DbSet<TenantConnectionString> TenantConnectionStrings { get; set; }
@ -18,7 +22,7 @@ namespace Volo.Abp.MultiTenancy.EntityFrameworkCore
{
base.OnModelCreating(builder);
this.ConfigureMultiTenancy(builder);
this.ConfigureMultiTenancy(builder, TablePrefix, Schema);
}
}
}

@ -1,30 +1,37 @@
using System;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
namespace Volo.Abp.MultiTenancy.EntityFrameworkCore
{
public static class MultiTenancyDbContextModelCreatingExtensions
{
public static void ConfigureMultiTenancy(this IMultiTenancyDbContext dbContext, ModelBuilder builder)
public static void ConfigureMultiTenancy(this IMultiTenancyDbContext dbContext, ModelBuilder builder, string tablePrefix = "", [CanBeNull] string schema = null)
{
if (tablePrefix.IsNullOrWhiteSpace())
{
tablePrefix = "";
}
builder.Entity<Tenant>(b =>
{
b.ToTable("MtTenants"); //TODO: Make all table and schema names changeable
b.ToTable(tablePrefix + "Tenants", schema);
b.Property(t => t.Name).IsRequired().HasMaxLength(TenantConsts.MaxNameLength);
b.HasMany(u => u.ConnectionStrings).WithOne().HasForeignKey(uc => uc.TenantId).IsRequired();
b.HasIndex(u => u.Name);
b.HasIndex(u => u.Name).IsUnique();
});
builder.Entity<TenantConnectionString>(b =>
{
b.ToTable("MtTenantConnectionStrings");
b.ToTable(tablePrefix + "TenantConnectionStrings", schema);
b.HasKey(x => new {x.TenantId, x.Name});
b.Property(cs => cs.Name).IsRequired().HasMaxLength(TenantConnectionStringConsts.MaxNameLength);
b.Property(cs => cs.Value).IsRequired().HasMaxLength(TenantConnectionStringConsts.MaxValueLength);
b.HasIndex(cs => cs.TenantId);
});
}
}

@ -39,7 +39,7 @@ namespace Volo.Abp.Http.DynamicProxying
{
var people = await _peopleAppService.GetListAsync(new PagedAndSortedResultRequestDto());
people.TotalCount.ShouldBeGreaterThan(0);
people.Items.Count.ShouldBe(people.TotalCount);
people.Items.Count.ShouldBe((int) people.TotalCount);
}
[Fact]

@ -1,7 +1,6 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
using Shouldly;
@ -14,8 +13,8 @@ namespace Volo.Abp.Identity
public IdentityRoleAppService_Tests()
{
_roleAppService = ServiceProvider.GetRequiredService<IIdentityRoleAppService>();
_roleRepository = ServiceProvider.GetRequiredService<IIdentityRoleRepository>();
_roleAppService = GetRequiredService<IIdentityRoleAppService>();
_roleRepository = GetRequiredService<IIdentityRoleRepository>();
}
[Fact]

@ -1,9 +1,6 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Shouldly;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Domain.Entities;
using Xunit;
@ -16,8 +13,8 @@ namespace Volo.Abp.Identity
public IdentityUserAppService_Tests()
{
_userAppService = ServiceProvider.GetRequiredService<IIdentityUserAppService>();
_userRepository = ServiceProvider.GetRequiredService<IIdentityUserRepository>();
_userAppService = GetRequiredService<IIdentityUserAppService>();
_userRepository = GetRequiredService<IIdentityUserRepository>();
}
[Fact]

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<AssemblyName>Volo.Abp.MultiTenancy.Application.Tests</AssemblyName>
<PackageId>Volo.Abp.MultiTenancy.Application.Tests</PackageId>
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Volo.Abp.Autofac\Volo.Abp.Autofac.csproj" />
<ProjectReference Include="..\AbpTestBase\AbpTestBase.csproj" />
<ProjectReference Include="..\..\src\Volo.Abp.MultiTenancy.Application\Volo.Abp.MultiTenancy.Application.csproj" />
<ProjectReference Include="..\..\src\Volo.Abp.MultiTenancy.EntityFrameworkCore\Volo.Abp.MultiTenancy.EntityFrameworkCore.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.3.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="2.0.0" />
</ItemGroup>
</Project>

@ -0,0 +1,30 @@
using System;
using Volo.Abp.MultiTenancy.EntityFrameworkCore;
using Volo.Abp.TestBase;
namespace Volo.Abp.MultiTenancy
{
public class AbpMultiTenancyApplicationTestBase : AbpIntegratedTest<AbpMultiTenancyApplicationTestModule>
{
protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options)
{
options.UseAutofac();
}
protected virtual void UsingDbContext(Action<IMultiTenancyDbContext> action)
{
using (var dbContext = GetRequiredService<IMultiTenancyDbContext>())
{
action.Invoke(dbContext);
}
}
protected virtual T UsingDbContext<T>(Func<IMultiTenancyDbContext, T> action)
{
using (var dbContext = GetRequiredService<IMultiTenancyDbContext>())
{
return action.Invoke(dbContext);
}
}
}
}

@ -0,0 +1,55 @@
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Autofac;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.MultiTenancy.EntityFrameworkCore;
using Volo.Abp.Modularity;
using Volo.Abp.Uow;
namespace Volo.Abp.MultiTenancy
{
[DependsOn(
typeof(AbpMultiTenancyApplicationModule),
typeof(AbpMultiTenancyEntityFrameworkCoreModule),
typeof(AbpAutofacModule))]
public class AbpMultiTenancyApplicationTestModule : AbpModule
{
public override void ConfigureServices(IServiceCollection services)
{
services.AddAssemblyOf<AbpMultiTenancyApplicationTestModule>();
services.AddEntityFrameworkInMemoryDatabase();
var databaseName = Guid.NewGuid().ToString();
services.Configure<AbpDbContextOptions>(options =>
{
options.Configure(context =>
{
context.DbContextOptions.UseInMemoryDatabase(databaseName);
});
});
services.Configure<UnitOfWorkDefaultOptions>(options =>
{
options.TransactionBehavior = UnitOfWorkTransactionBehavior.Disabled; //EF in-memory database does not support transactions
});
}
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
SeedTestData(context);
}
private static void SeedTestData(ApplicationInitializationContext context)
{
using (var scope = context.ServiceProvider.CreateScope())
{
scope.ServiceProvider
.GetRequiredService<AbpMultiTenancyTestDataBuilder>()
.Build();
}
}
}
}

@ -0,0 +1,34 @@
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Threading;
namespace Volo.Abp.MultiTenancy
{
public class AbpMultiTenancyTestDataBuilder : ITransientDependency
{
private readonly ITenantRepository _tenantRepository;
private readonly ITenantManager _tenantManager;
public AbpMultiTenancyTestDataBuilder(
ITenantRepository tenantRepository,
ITenantManager tenantManager)
{
_tenantRepository = tenantRepository;
_tenantManager = tenantManager;
}
public void Build()
{
AsyncHelper.RunSync(AddTenantsAsync);
}
private async Task AddTenantsAsync()
{
var acme = await _tenantManager.CreateAsync("acme");
await _tenantRepository.InsertAsync(acme);
var volosoft = await _tenantManager.CreateAsync("volosoft");
await _tenantRepository.InsertAsync(volosoft);
}
}
}

@ -0,0 +1,113 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Shouldly;
using Volo.Abp.Ui;
using Xunit;
namespace Volo.Abp.MultiTenancy
{
public class TenantAppService_Tests : AbpMultiTenancyApplicationTestBase
{
private readonly ITenantAppService _tenantAppService;
public TenantAppService_Tests()
{
_tenantAppService = GetRequiredService<ITenantAppService>();
}
[Fact]
public async Task GetAsync()
{
var tenantInDb = UsingDbContext(dbContext => dbContext.Tenants.First());
var tenant = await _tenantAppService.GetAsync(tenantInDb.Id);
tenant.Name.ShouldBe(tenantInDb.Name);
}
[Fact]
public async Task GetListAsync()
{
var result = await _tenantAppService.GetListAsync(new GetTenantsInput());
result.TotalCount.ShouldBeGreaterThan(0);
result.Items.ShouldContain(t => t.Name == "acme");
result.Items.ShouldContain(t => t.Name == "volosoft");
}
[Fact]
public async Task GetListAsync_Filtered()
{
var result = await _tenantAppService.GetListAsync(new GetTenantsInput { Filter = "volo" });
result.TotalCount.ShouldBeGreaterThan(0);
result.Items.ShouldNotContain(t => t.Name == "acme");
result.Items.ShouldContain(t => t.Name == "volosoft");
}
[Fact]
public async Task GetListAsync_Sorted_Descending_By_Name()
{
var result = await _tenantAppService.GetListAsync(new GetTenantsInput { Sorting = "Name DESC" });
result.TotalCount.ShouldBeGreaterThan(0);
var tenants = result.Items.ToList();
tenants.ShouldContain(t => t.Name == "acme");
tenants.ShouldContain(t => t.Name == "volosoft");
tenants.FindIndex(t => t.Name == "acme").ShouldBeGreaterThan(tenants.FindIndex(t => t.Name == "volosoft"));
}
[Fact]
public async Task CreateAsync()
{
var tenancyName = Guid.NewGuid().ToString("N").ToLowerInvariant();
var tenant = await _tenantAppService.CreateAsync(new TenantCreateDto { Name = tenancyName });
tenant.Name.ShouldBe(tenancyName);
tenant.Id.ShouldNotBe(default(Guid));
}
[Fact]
public async Task CreateAsync_Should_Not_Allow_Duplicate_Names()
{
await Assert.ThrowsAsync<UserFriendlyException>(async () =>
{
await _tenantAppService.CreateAsync(new TenantCreateDto { Name = "acme" });
});
}
[Fact]
public async Task UpdateAsync()
{
var acme = UsingDbContext(dbContext => dbContext.Tenants.Single(t => t.Name == "acme"));
var result = await _tenantAppService.UpdateAsync(acme.Id, new TenantUpdateDto { Name = "acme-renamed" });
result.Id.ShouldBe(acme.Id);
result.Name.ShouldBe("acme-renamed");
var acmeUpdated = UsingDbContext(dbContext => dbContext.Tenants.Single(t => t.Id == acme.Id));
acmeUpdated.Name.ShouldBe("acme-renamed");
}
[Fact]
public async Task UpdateAsync_Should_Not_Allow_Duplicate_Names()
{
var acme = UsingDbContext(dbContext => dbContext.Tenants.Single(t => t.Name == "acme"));
await Assert.ThrowsAsync<UserFriendlyException>(async () =>
{
await _tenantAppService.UpdateAsync(acme.Id, new TenantUpdateDto { Name = "volosoft" });
});
}
[Fact]
public async Task DeleteAsync()
{
var acme = UsingDbContext(dbContext => dbContext.Tenants.Single(t => t.Name == "acme"));
await _tenantAppService.DeleteAsync(acme.Id);
UsingDbContext(dbContext =>
{
dbContext.Tenants.Any(t => t.Id == acme.Id).ShouldBeFalse();
});
}
}
}
Loading…
Cancel
Save