Я хотел узнать, как создать API, включающий ASP.NET Core API 2.1 + Dapper + DbUp + Azure Key Vault
и размещенный внутри Azure App Service (Web App)
, а также управлять им внутри Azure DevOps project with CI/CD
.
Я использую 2 среды (Разработка и Производство), и в Azure App Service (Web App)
, где я это развернул, я установил ASPNETCORE_ENVIRONMENT
со значением, установленным для Производство
В Azure DevOps я выбрал ASP.NET Core Template
для своего конвейера сборки (я использую конструктор, а не YAML), и я столкнулся с проблемой при запуске «тестов».
Тесты, которые я реализовал, являются "интеграционными тестами", и они всегда выполняются в среде Development
. Чтобы решить эту проблему, я реализовал пользовательский WebApplicationFactory
, как указано в ссылках ниже,
Сможете ли вы помочь мне решить проблему, пожалуйста?
Ниже приведены фрагменты кода в моем приложении.
- appsettings.Development.json
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
},
"DatabaseConfig": {
"ConnectionString": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=BlogDb;Integrated Security=True;Connect Timeout=30;"
}
}
- appsettings.Production.json
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
},
"AzureKeyVaultUrl": "THE AKV URL",
"DatabaseConfig": {
"ConnectionString": "BlogConnectionString"
}
}
- Мои настройки базы данных находятся в классе
DatabaseConfig
, и в режиме разработки NON он будет использовать AKV
для получения данных строки подключения.
То есть в Program.cs
public static IWebHostBuilder CreateWebHostBuilder(string[] args)
{
return WebHost.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((context, builder) =>
{
if (!context.HostingEnvironment.IsDevelopment())
{
var akvUrl = builder.Build()[AzureKeyVaultUrl];
var tokenProvider = new AzureServiceTokenProvider();
var keyVaultClient = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(tokenProvider.KeyVaultTokenCallback));
builder.AddAzureKeyVault(akvUrl, keyVaultClient, new DefaultKeyVaultSecretManager());
}
})
.UseStartup<Startup>();
}
В Startup.cs
public void ConfigureDevelopmentServices(IServiceCollection services)
{
RegisterDevDepdendencies(services);
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
public void ConfigureProductionServices(IServiceCollection services)
{
RegisterDependencies(services);
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
private void RegisterDevDepdendencies(IServiceCollection services)
{
//
// Register configurations
//
services.Configure<DatabaseConfig>(Configuration.GetSection("DatabaseConfig"));
services.AddSingleton(provider =>
{
var dbConfig = provider.GetRequiredService<IOptions<DatabaseConfig>>().Value;
return dbConfig;
});
//
// Register repositories
//
services.AddTransient<IPostsRepository, PostsRepository>();
//
// Register filters
//
services.AddTransient<IStartupFilter, DevDatabaseInitFilter>();
}
private void RegisterDependencies(IServiceCollection services)
{
//
// Register configurations
//
services.Configure<DatabaseConfig>(Configuration.GetSection("DatabaseConfig"));
services.AddSingleton(provider =>
{
var dbConfig = provider.GetRequiredService<IOptions<DatabaseConfig>>().Value;
var connectionString = Configuration[dbConfig.ConnectionString];
return new DatabaseConfig {ConnectionString = connectionString};
});
//
// Register repositories
//
services.AddTransient<IPostsRepository, PostsRepository>();
//
// Register filters
//
services.AddTransient<IStartupFilter, DatabaseInitFilter>();
}
Реализации фильтра запуска
Фильтры запуска должны выполнять миграцию с использованием DbUp. Я запускаю это как задачу запуска (это было вдохновлено после прочтения потрясающей статьи от Andrew Lock ). Ниже приведены их реализации.
public class DevDatabaseInitFilter : IStartupFilter
{
private readonly string _connectionString;
public DevDatabaseInitFilter(DatabaseConfig config)
{
_connectionString = config?.ConnectionString;
if (string.IsNullOrEmpty(_connectionString))
{
throw new ArgumentNullException(nameof(config));
}
}
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
{
EnsureDatabase.For.SqlDatabase(_connectionString);
var dbUpgradeEngineBuilder = DeployChanges.To
.SqlDatabase(_connectionString)
.WithScriptsEmbeddedInAssembly(typeof(DatabaseInitFilter).Assembly)
.WithTransaction()
.LogToAutodetectedLog();
var dbUpgradeEngine = dbUpgradeEngineBuilder.Build();
if (dbUpgradeEngine.IsUpgradeRequired())
{
var upgradeOperation = dbUpgradeEngine.PerformUpgrade();
if (!upgradeOperation.Successful)
{
throw new Exception("Database upgrade unsuccessful", upgradeOperation.Error);
}
}
return next;
}
}
public class DatabaseInitFilter : IStartupFilter
{
private readonly string _connectionString;
public DatabaseInitFilter(DatabaseConfig config)
{
_connectionString = config?.ConnectionString;
if (string.IsNullOrEmpty(_connectionString))
{
throw new ArgumentNullException(nameof(config));
}
}
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
{
EnsureDatabase.For.SqlDatabase(_connectionString);
var dbUpgradeEngineBuilder = DeployChanges.To
.SqlDatabase(_connectionString)
.WithScriptsEmbeddedInAssembly(typeof(DatabaseInitFilter).Assembly)
.WithTransaction()
.LogToAutodetectedLog();
var dbUpgradeEngine = dbUpgradeEngineBuilder.Build();
if (dbUpgradeEngine.IsUpgradeRequired())
{
var upgradeOperation = dbUpgradeEngine.PerformUpgrade();
if (!upgradeOperation.Successful)
{
throw new Exception("Database upgrade unsuccessful", upgradeOperation.Error);
}
}
return next;
}
}
И, наконец, связанные с тестом классы
public class TestWebApplicationFactory<TEntryPoint> : WebApplicationFactory<TEntryPoint> where TEntryPoint : class
{
private const string AspNetCoreEnvironment = "ASPNETCORE_ENVIRONMENT";
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
base.ConfigureWebHost(builder);
var env = Environment.GetEnvironmentVariable(AspNetCoreEnvironment);
if (!string.IsNullOrEmpty(env))
{
builder.UseEnvironment(env);
}
}
}
public class PostsControllerTests : IClassFixture<TestWebApplicationFactory<Startup>>
{
private HttpClient _client;
public PostsControllerTests(TestWebApplicationFactory<Startup> factory)
{
_client = factory.CreateClient();
}
[Fact]
public async Task Must_Have_Posts()
{
//
// Arrange and Act
//
var httpResponse = await _client.GetAsync(@"/api/posts").ConfigureAwait(false);
//
// Assert
//
httpResponse.EnsureSuccessStatusCode();
}
}