This is sixth post of the series: .NET Microservice with ABP
Posts in the Series
Part 3. Administration Service
Part 6. DB Migration (this post)
Part 8. Identity server and Angular App
Table of contents
- Add the references
- Create
DbMigrationService
- Update appsettings.json
- Create
IdentityServerDataSeedContributor
for identity server - Create
IdentityServerDataSeeder
for reading json and create resource - Update the
TaskyDbMigratorModule
Add the references
For migrations we need to first add contracts and EF core projects as a reference in the db migrator. We need to do this for all the services.
1 <ItemGroup> 2 <ProjectReference Include="..\..\services\AdministrationService\src\Tasky.AdministrationService.Application.Contracts\Tasky.AdministrationService.Application.Contracts.csproj"/> 3 <ProjectReference Include="..\..\services\AdministrationService\src\Tasky.AdministrationService.EntityFrameworkCore\Tasky.AdministrationService.EntityFrameworkCore.csproj"/> 4 <ProjectReference Include="..\..\services\identity\src\Tasky.IdentityService.Application.Contracts\Tasky.IdentityService.Application.Contracts.csproj"/> 5 <ProjectReference Include="..\..\services\identity\src\Tasky.IdentityService.EntityFrameworkCore\Tasky.IdentityService.EntityFrameworkCore.csproj"/> 6 <ProjectReference Include="..\..\services\SaaSService\src\Tasky.SaaSService.Application.Contracts\Tasky.SaaSService.Application.Contracts.csproj"/> 7 <ProjectReference Include="..\..\services\SaaSService\src\Tasky.SaaSService.EntityFrameworkCore\Tasky.SaaSService.EntityFrameworkCore.csproj"/> 8 </ItemGroup>
We are adding EntityFrameworkCore
and Contracts
projects to the DbMigrations project.
DbMigrationService
Create Db migration service is where we can manage the migration and seeding of data of all the services in a single location.
1using System; 2using System.Collections.Generic; 3using System.Threading; 4using System.Threading.Tasks; 5using Microsoft.EntityFrameworkCore; 6using Microsoft.Extensions.DependencyInjection; 7using Microsoft.Extensions.Logging; 8using Tasky.AdministrationService.EntityFrameworkCore; 9using Tasky.IdentityService.EntityFrameworkCore; 10using Tasky.SaaSService.EntityFrameworkCore; 11using Volo.Abp.Data; 12using Volo.Abp.DependencyInjection; 13using Volo.Abp.EntityFrameworkCore; 14using Volo.Abp.Identity; 15using Volo.Abp.MultiTenancy; 16using Volo.Abp.TenantManagement; 17using Volo.Abp.Uow; 18 19namespace Tasky.DbMigrator; 20 21public class TaskyDbMigrationService : ITransientDependency 22{ 23 private readonly ICurrentTenant _currentTenant; 24 private readonly IDataSeeder _dataSeeder; 25 private readonly ILogger<TaskyDbMigrationService> _logger; 26 private readonly ITenantRepository _tenantRepository; 27 private readonly IUnitOfWorkManager _unitOfWorkManager; 28 29 public TaskyDbMigrationService( 30 ILogger<TaskyDbMigrationService> logger, 31 ITenantRepository tenantRepository, 32 IDataSeeder dataSeeder, 33 ICurrentTenant currentTenant, 34 IUnitOfWorkManager unitOfWorkManager) 35 { 36 _logger = logger; 37 _tenantRepository = tenantRepository; 38 _dataSeeder = dataSeeder; 39 _currentTenant = currentTenant; 40 _unitOfWorkManager = unitOfWorkManager; 41 } 42 43 public async Task MigrateAsync(CancellationToken cancellationToken) 44 { 45 await MigrateHostAsync(cancellationToken); 46 await MigrateTenantsAsync(cancellationToken); 47 _logger.LogInformation("Migration completed!"); 48 } 49 50 private async Task MigrateHostAsync(CancellationToken cancellationToken) 51 { 52 _logger.LogInformation("Migrating Host side..."); 53 await MigrateAllDatabasesAsync(null, cancellationToken); 54 await SeedDataAsync(); 55 } 56 57 private async Task MigrateTenantsAsync(CancellationToken cancellationToken) 58 { 59 _logger.LogInformation("Migrating tenants..."); 60 61 var tenants = 62 await _tenantRepository.GetListAsync(includeDetails: true, cancellationToken: cancellationToken); 63 var migratedDatabaseSchemas = new HashSet<string>(); 64 foreach (var tenant in tenants) 65 { 66 using (_currentTenant.Change(tenant.Id)) 67 { 68 // Database schema migration 69 var connectionString = tenant.FindDefaultConnectionString(); 70 if (!connectionString.IsNullOrWhiteSpace() && //tenant has a separate database 71 !migratedDatabaseSchemas.Contains(connectionString)) //the database was not migrated yet 72 { 73 _logger.LogInformation($"Migrating tenant database: {tenant.Name} ({tenant.Id})"); 74 await MigrateAllDatabasesAsync(tenant.Id, cancellationToken); 75 migratedDatabaseSchemas.AddIfNotContains(connectionString); 76 } 77 78 //Seed data 79 _logger.LogInformation($"Seeding tenant data: {tenant.Name} ({tenant.Id})"); 80 await SeedDataAsync(); 81 } 82 } 83 } 84 85 private async Task MigrateAllDatabasesAsync( 86 Guid? tenantId, 87 CancellationToken cancellationToken) 88 { 89 using (var uow = _unitOfWorkManager.Begin(true)) 90 { 91 if (tenantId == null) 92 { 93 /* SaaSService schema should only be available in the host side */ 94 await MigrateDatabaseAsync<SaaSServiceDbContext>(cancellationToken); 95 } 96 97 await MigrateDatabaseAsync<AdministrationServiceDbContext>(cancellationToken); 98 await MigrateDatabaseAsync<IdentityServiceDbContext>(cancellationToken); 99 100 await uow.CompleteAsync(cancellationToken); 101 } 102 103 _logger.LogInformation( 104 $"All databases have been successfully migrated ({(tenantId.HasValue ? $"tenantId: {tenantId}" : "HOST")})."); 105 } 106 107 private async Task MigrateDatabaseAsync<TDbContext>( 108 CancellationToken cancellationToken) 109 where TDbContext : DbContext, IEfCoreDbContext 110 { 111 _logger.LogInformation($"Migrating {typeof(TDbContext).Name.RemovePostFix("DbContext")} database..."); 112 113 var dbContext = await _unitOfWorkManager.Current.ServiceProvider 114 .GetRequiredService<IDbContextProvider<TDbContext>>() 115 .GetDbContextAsync(); 116 117 await dbContext 118 .Database 119 .MigrateAsync(cancellationToken); 120 } 121 122 private async Task SeedDataAsync() 123 { 124 await _dataSeeder.SeedAsync( 125 new DataSeedContext(_currentTenant.Id) 126 .WithProperty(IdentityDataSeedContributor.AdminEmailPropertyName, "admin@abp.io") 127 .WithProperty(IdentityDataSeedContributor.AdminPasswordPropertyName, "1q2w3E*") 128 ); 129 } 130}
Update appsettings.json
The appsettings has a different structure form the normal structure you will usually see in the abp projects. We use this as a data source to seed the ApiScope
, ApiResource
and Identity server clients
.
1{ 2 "ConnectionStrings": { 3 "SaaSService": "User ID=postgres;Password=postgres;Host=localhost;Port=5432;Database=TaskySaaSService;Pooling=false;", 4 "IdentityService": "User ID=postgres;Password=postgres;Host=localhost;Port=5432;Database=TaskyIdentityService;Pooling=false;", 5 "AdministrationService": "User ID=postgres;Password=postgres;Host=localhost;Port=5432;Database=TaskyAdministrationService;Pooling=false;" 6 }, 7 "ApiScope": [ 8 "AuthServer", 9 "SaaSService", 10 "IdentityService", 11 "AdministrationService" 12 ], 13 "ApiResource": [ 14 "AuthServer", 15 "SaaSService", 16 "IdentityService", 17 "AdministrationService" 18 ], 19 "Clients": [ 20 { 21 "ClientId": "Tasky_Web", 22 "ClientSecret": "1q2w3e*", 23 "RootUrls": [ 24 "https://localhost:7004" 25 ], 26 "Scopes": [ 27 "SaaSService", 28 "IdentityService", 29 "AdministrationService" 30 ], 31 "GrantTypes": [ 32 "hybrid" 33 ], 34 "RedirectUris": [ 35 "https://localhost:7004/signin-oidc" 36 ], 37 "PostLogoutRedirectUris": [ 38 "https://localhost:7004/signout-callback-oidc" 39 ], 40 "AllowedCorsOrigins": [ 41 "https://localhost:7004" 42 ] 43 }, 44 { 45 "ClientId": "Tasky_App", 46 "ClientSecret": "1q2w3e*", 47 "RootUrls": [ 48 "http://localhost:4200" 49 ], 50 "Scopes": [ 51 "AuthServer", 52 "SaaSService", 53 "IdentityService", 54 "AdministrationService" 55 ], 56 "GrantTypes": [ 57 "authorization_code" 58 ], 59 "RedirectUris": [ 60 "http://localhost:4200" 61 ], 62 "PostLogoutRedirectUris": [ 63 "http://localhost:4200" 64 ], 65 "AllowedCorsOrigins": [ 66 "http://localhost:4200" 67 ] 68 }, 69 { 70 "ClientId": "AdministrationService_Swagger", 71 "ClientSecret": "1q2w3e*", 72 "RootUrls": [ 73 "https://localhost:7001" 74 ], 75 "Scopes": [ 76 "SaaSService", 77 "IdentityService", 78 "AdministrationService" 79 ], 80 "GrantTypes": [ 81 "authorization_code" 82 ], 83 "RedirectUris": [ 84 "https://localhost:7001/swagger/oauth2-redirect.html" 85 ], 86 "PostLogoutRedirectUris": [ 87 "https://localhost:7001/signout-callback-oidc" 88 ], 89 "AllowedCorsOrigins": [ 90 "https://localhost:7001" 91 ] 92 }, 93 { 94 "ClientId": "IdentityService_Swagger", 95 "ClientSecret": "1q2w3e*", 96 "RootUrls": [ 97 "https://localhost:7002" 98 ], 99 "Scopes": [ 100 "SaaSService", 101 "IdentityService", 102 "AdministrationService" 103 ], 104 "GrantTypes": [ 105 "authorization_code" 106 ], 107 "RedirectUris": [ 108 "https://localhost:7002/swagger/oauth2-redirect.html" 109 ], 110 "PostLogoutRedirectUris": [ 111 "https://localhost:7002" 112 ], 113 "AllowedCorsOrigins": [ 114 "https://localhost:7002" 115 ] 116 }, 117 { 118 "ClientId": "SaaSService_Swagger", 119 "ClientSecret": "1q2w3e*", 120 "RootUrls": [ 121 "https://localhost:7003" 122 ], 123 "Scopes": [ 124 "SaaSService", 125 "IdentityService", 126 "AdministrationService" 127 ], 128 "GrantTypes": [ 129 "authorization_code" 130 ], 131 "RedirectUris": [ 132 "https://localhost:7003/swagger/oauth2-redirect.html" 133 ], 134 "PostLogoutRedirectUris": [ 135 "https://localhost:7003" 136 ], 137 "AllowedCorsOrigins": [ 138 "https://localhost:7003" 139 ] 140 } 141 ] 142}
IdentityServerDataSeedContributor
for identity server
Create 1using System.Threading.Tasks; 2using Volo.Abp.Data; 3using Volo.Abp.DependencyInjection; 4 5namespace Tasky.DbMigrator; 6 7public class IdentityServerDataSeedContributor : IDataSeedContributor, ITransientDependency 8{ 9 private readonly IdentityServerDataSeeder _identityServerDataSeeder; 10 11 public IdentityServerDataSeedContributor(IdentityServerDataSeeder identityServerDataSeeder) 12 { 13 _identityServerDataSeeder = identityServerDataSeeder; 14 } 15 16 17 public async Task SeedAsync(DataSeedContext context) 18 { 19 await _identityServerDataSeeder.SeedAsync(); 20 } 21}
IdentityServerDataSeeder
for reading json and create resource
Create This service seeds the ApiScope
, ApiResource
and Identity server clients
for the Identity server.
ServiceClient
is a class to parse the Identity server clients
1public class ServiceClient 2{ 3 public string ClientId { get; set; } 4 public string ClientSecret { get; set; } 5 public string[] RootUrls { get; set; } 6 public string[] Scopes { get; set; } 7 public string[] GrantTypes { get; set; } 8 public string[] RedirectUris { get; set; } 9 public string[] PostLogoutRedirectUris { get; set; } 10 public string[] AllowedCorsOrigins { get; set; } 11}
1using System; 2using System.Collections.Generic; 3using System.Linq; 4using System.Threading.Tasks; 5using IdentityServer4.Models; 6using Microsoft.Extensions.Configuration; 7using Volo.Abp.Authorization.Permissions; 8using Volo.Abp.DependencyInjection; 9using Volo.Abp.Guids; 10using Volo.Abp.IdentityServer.ApiResources; 11using Volo.Abp.IdentityServer.ApiScopes; 12using Volo.Abp.IdentityServer.Clients; 13using Volo.Abp.IdentityServer.IdentityResources; 14using Volo.Abp.MultiTenancy; 15using Volo.Abp.PermissionManagement; 16using Volo.Abp.Uow; 17using ApiResource = Volo.Abp.IdentityServer.ApiResources.ApiResource; 18using ApiScope = Volo.Abp.IdentityServer.ApiScopes.ApiScope; 19using Client = Volo.Abp.IdentityServer.Clients.Client; 20 21namespace Tasky.DbMigrator; 22 23public class IdentityServerDataSeeder : ITransientDependency 24{ 25 private readonly IApiResourceRepository _apiResourceRepository; 26 private readonly IApiScopeRepository _apiScopeRepository; 27 private readonly IClientRepository _clientRepository; 28 private readonly IConfiguration _configuration; 29 private readonly ICurrentTenant _currentTenant; 30 private readonly IGuidGenerator _guidGenerator; 31 private readonly IIdentityResourceDataSeeder _identityResourceDataSeeder; 32 private readonly IPermissionDataSeeder _permissionDataSeeder; 33 34 public IdentityServerDataSeeder( 35 IClientRepository clientRepository, 36 IApiResourceRepository apiResourceRepository, 37 IApiScopeRepository apiScopeRepository, 38 IIdentityResourceDataSeeder identityResourceDataSeeder, 39 IGuidGenerator guidGenerator, 40 IPermissionDataSeeder permissionDataSeeder, 41 IConfiguration configuration, 42 ICurrentTenant currentTenant) 43 { 44 _clientRepository = clientRepository; 45 _apiResourceRepository = apiResourceRepository; 46 _apiScopeRepository = apiScopeRepository; 47 _identityResourceDataSeeder = identityResourceDataSeeder; 48 _guidGenerator = guidGenerator; 49 _permissionDataSeeder = permissionDataSeeder; 50 _configuration = configuration; 51 _currentTenant = currentTenant; 52 } 53 54 [UnitOfWork] 55 public async virtual Task SeedAsync() 56 { 57 using (_currentTenant.Change(null)) 58 { 59 await _identityResourceDataSeeder.CreateStandardResourcesAsync(); 60 await CreateApiResourcesAsync(); 61 await CreateApiScopesAsync(); 62 await CreateClientsAsync(); 63 } 64 } 65 66 private async Task CreateClientsAsync() 67 { 68 var clients = _configuration.GetSection("Clients").Get<List<ServiceClient>>(); 69 var commonScopes = new[] { 70 "email", 71 "openid", 72 "profile", 73 "role", 74 "phone", 75 "address" 76 }; 77 78 foreach (var client in clients) 79 { 80 await CreateClientAsync( 81 client.ClientId, 82 commonScopes.Union(client.Scopes), 83 client.GrantTypes, 84 client.ClientSecret.Sha256(), 85 requireClientSecret: false, 86 redirectUris: client.RedirectUris, 87 postLogoutRedirectUris: client.PostLogoutRedirectUris, 88 corsOrigins: client.AllowedCorsOrigins 89 ); 90 } 91 } 92 93 94 private async Task CreateApiResourcesAsync() 95 { 96 var commonApiUserClaims = new[] { 97 "email", 98 "email_verified", 99 "name", 100 "phone_number", 101 "phone_number_verified", 102 "role" 103 }; 104 105 var apiResources = _configuration.GetSection("ApiResource").Get<string[]>(); 106 107 foreach (var item in apiResources) 108 { 109 await CreateApiResourceAsync(item, commonApiUserClaims); 110 } 111 } 112 113 private async Task CreateApiScopesAsync() 114 { 115 var apiScopes = _configuration.GetSection("ApiScope").Get<string[]>(); 116 foreach (var item in apiScopes) 117 { 118 await CreateApiScopeAsync(item); 119 } 120 } 121 122 private async Task<ApiResource> CreateApiResourceAsync(string name, IEnumerable<string> claims) 123 { 124 var apiResource = await _apiResourceRepository.FindByNameAsync(name); 125 if (apiResource == null) 126 { 127 apiResource = await _apiResourceRepository.InsertAsync( 128 new ApiResource( 129 _guidGenerator.Create(), 130 name, 131 name + " API" 132 ), 133 true 134 ); 135 } 136 137 foreach (var claim in claims) 138 { 139 if (apiResource.FindClaim(claim) == null) 140 { 141 apiResource.AddUserClaim(claim); 142 } 143 } 144 145 return await _apiResourceRepository.UpdateAsync(apiResource); 146 } 147 148 private async Task<ApiScope> CreateApiScopeAsync(string name) 149 { 150 var apiScope = await _apiScopeRepository.FindByNameAsync(name); 151 if (apiScope == null) 152 { 153 apiScope = await _apiScopeRepository.InsertAsync( 154 new ApiScope( 155 _guidGenerator.Create(), 156 name, 157 name + " API" 158 ), 159 true 160 ); 161 } 162 163 return apiScope; 164 } 165 166 private async Task<Client> CreateClientAsync( 167 string name, 168 IEnumerable<string> scopes, 169 IEnumerable<string> grantTypes, 170 string secret = null, 171 IEnumerable<string> redirectUris = null, 172 IEnumerable<string> postLogoutRedirectUris = null, 173 string frontChannelLogoutUri = null, 174 bool requireClientSecret = true, 175 bool requirePkce = false, 176 IEnumerable<string> permissions = null, 177 IEnumerable<string> corsOrigins = null) 178 { 179 var client = await _clientRepository.FindByClientIdAsync(name); 180 if (client == null) 181 { 182 client = await _clientRepository.InsertAsync( 183 new Client( 184 _guidGenerator.Create(), 185 name 186 ) { 187 ClientName = name, 188 ProtocolType = "oidc", 189 Description = name, 190 AlwaysIncludeUserClaimsInIdToken = true, 191 AllowOfflineAccess = true, 192 AbsoluteRefreshTokenLifetime = 31536000, //365 days 193 AccessTokenLifetime = 31536000, //365 days 194 AuthorizationCodeLifetime = 300, 195 IdentityTokenLifetime = 300, 196 RequireConsent = false, 197 FrontChannelLogoutUri = frontChannelLogoutUri, 198 RequireClientSecret = requireClientSecret, 199 RequirePkce = requirePkce 200 }, 201 true 202 ); 203 } 204 205 foreach (var scope in scopes) 206 { 207 if (client.FindScope(scope) == null) 208 { 209 client.AddScope(scope); 210 } 211 } 212 213 foreach (var grantType in grantTypes) 214 { 215 if (client.FindGrantType(grantType) == null) 216 { 217 client.AddGrantType(grantType); 218 } 219 } 220 221 if (!secret.IsNullOrEmpty()) 222 { 223 if (client.FindSecret(secret) == null) 224 { 225 client.AddSecret(secret); 226 } 227 } 228 229 foreach (var redirectUrl in redirectUris) 230 { 231 if (client.FindRedirectUri(redirectUrl) == null) 232 { 233 client.AddRedirectUri(redirectUrl); 234 } 235 } 236 237 foreach (var postLogoutRedirectUri in postLogoutRedirectUris) 238 { 239 if (client.FindPostLogoutRedirectUri(postLogoutRedirectUri) == null) 240 { 241 client.AddPostLogoutRedirectUri(postLogoutRedirectUri); 242 } 243 } 244 245 if (permissions != null) 246 { 247 await _permissionDataSeeder.SeedAsync( 248 ClientPermissionValueProvider.ProviderName, 249 name, 250 permissions 251 ); 252 } 253 254 if (corsOrigins != null) 255 { 256 foreach (var origin in corsOrigins) 257 { 258 if (!origin.IsNullOrWhiteSpace() && client.FindCorsOrigin(origin) == null) 259 { 260 client.AddCorsOrigin(origin); 261 } 262 } 263 } 264 265 return await _clientRepository.UpdateAsync(client); 266 } 267}
TaskyDbMigratorModule
Update the 1typeof(AdministrationServiceEntityFrameworkCoreModule), 2typeof(AdministrationServiceApplicationContractsModule), 3typeof(IdentityServiceEntityFrameworkCoreModule), 4typeof(IdentityServiceApplicationContractsModule), 5typeof(SaaSServiceEntityFrameworkCoreModule), 6typeof(SaaSServiceApplicationContractsModule)