Introdução
Para quem já trabalhou com o Entity Framework Core utilizando o modelo Code-First, o Migrations foi uma sacada bem legal para “versionar” através da aplicação, os estados da estrutura de seu banco de dados. Utilizando os comando CLI, por exemplo: dotnet ef migrations add application_v1
ou dotnet ef database update
, podemos refletir as mudanças criadas a partir do modelo de classes no banco de dados e subir automaticamente entre os ambientes de desenvolvimento, testes e produção. No entanto, ao executar estes comandos nos diferentes ambientes, o Entity Framework não saberá em qual banco de dados se conectar para fazer as atualizações a menos que você diga explicitamente a ele. Para isto foi criada a interface IDesignTimeDbContextFactory<SeuDbContext>
, e através dela é possível configurar para olhar diferentes connection strings de seus ambientes.
Neste artigo irei mostrar uma implementação efetiva de um DesignTimeDbContextFactory
genérico que suporta ambientes de: desenvolvimento e produção e que lê connection strings utilizando os arquivos appsettings.Development.json
e appsettings.json
. Para simplificar, utilizarei o EF Core com o SQLite, e todo código estará disponível neste repositório.
Criação do projeto
Crie um projeto do tipo WebApi com o nome DesignTimeExample na IDE/editor de preferência. Dê preferência a seguinte estrutura de projeto:
Exemplo de estrutura de projeto
Inclua um diretório na raíz do projeto com o nome: Data. Nele iremos guardar todos as classes relativas ao acesso de dados, como por exemplo o DbContext e nossa implementação do DesignTimeDbContextFactory. Crie também dentro da pasta Data, a pasta: Migrations. Ela servirá para guardar os snapshots (ou fotos) do estado do nosso banco de dados. Importante observar que é apesar de serem arquivos auto-gerados, eles definem o estado da aplicação, então é necessário sim versioná-los.
Inclua também um diretório chamado: Models. Nele iremos guardar todas as classes relativas ao modelo do negócio ou POCOs, irei usar um modelo bem simples de Avião e Voo, representados por: Plane
e Flight
.
Criação dos Models
As classes a seguir servem como modelos para interagir com as tabelas no banco de dados:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
using System.Collections.Generic;
namespace DesignTimeExample.Models
{
public class Plane
{
public int Id { get; set; }
public int SeatsNumber { get; set; }
public PlaneModel Model { get; set; }
public ICollection<Flight> Flights { get; set; }
}
public enum PlaneModel
{
AIRBUS_A380,
BOEING_707,
AIRBUS_A320,
BOEING_727,
BOEING_767,
BOEING_757,
BOEING_787,
BOEING_737,
BOEING_777,
BOEING_747
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
using System.ComponentModel.DataAnnotations;
namespace DesignTimeExample.Models
{
public class Flight
{
public int Id { get; set; }
public string Code { get; set; }
[Required]
public Plane Plane { get; set; }
public int PassengerNumber { get; set; }
}
}
Acesso aos dados
Neste exemplo não iremos configurar um mapeamento das classes de modelo para um padrão de nomenclatura de banco de dados, portanto, as tabelas serão geradas exatamente como as classes.
DbContext
Para interagir com o banco de dados, precisamos criar um DbContext
da aplicação, então crie o arquivo ApplicationDbContext.cs
no diretório Data conforme a seguir:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using System.Reflection;
using DesignTimeExample.Models;
using Microsoft.EntityFrameworkCore;
namespace DesignTimeExample.Data
{
public class ApplicationDbContext : DbContext
{
public DbSet<Plane> Planes { get; set; }
public DbSet<Flight> Flights { get; set; }
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
}
}
}
Connection String
Agora vamos configurar a connection string em nosso arquivo de configuração para o ambiente de desenvolvimento. Edite o appsettings.Development.json
conforme a seguir:
1
2
3
4
5
6
7
8
9
10
11
12
{
"ConnectionStrings": {
"DefaultConnection": "Data Source=application_dev.db"
},
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
}
}
Iremos simular que este será nosso banco de dados de desenvolvimento. O de produção será o appsettings.json
, conforme a seguir:
1
2
3
4
5
6
7
8
9
10
11
{
"ConnectionStrings": {
"DefaultConnection": "Data Source=application_prod.db"
},
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*"
}
Naturalmente num cenário real, as connection strings apontarão para diferentes máquinas, pois desenvolvimento e produção estarão em infraestruturas segregadas. Mas para apenas simular esta separação, os nomes dos arquivos gerados do banco de dados SQLite serão application_dev.db
para desenvolvimento e application_prod.db
para produção.
DesignTimeDbContextFactory
Para identificar as diferentes connection strings ao rodar os comandos CLI do EF Core, necessitamos implementar a interface IDesignTimeDbContextFactory
. Para isto crie um arquivo chamado DesignTimeDbContextFactory.cs
no diretório Data
, conforme a seguir:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
using System;
using System.IO;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Configuration;
namespace DesignTimeExample.Data
{
public abstract class DesignTimeDbContextFactoryBase<TContext> :
IDesignTimeDbContextFactory<TContext> where TContext : DbContext
{
protected string ConnectionStringName { get; }
protected string MigrationsAssemblyName { get; }
public DesignTimeDbContextFactoryBase(string connectionStringName, string migrationsAssemblyName)
{
ConnectionStringName = connectionStringName;
MigrationsAssemblyName = migrationsAssemblyName;
}
public TContext CreateDbContext(string[] args)
{
return Create(
Directory.GetCurrentDirectory(),
Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"),
ConnectionStringName, MigrationsAssemblyName);
}
protected abstract TContext CreateNewInstance(
DbContextOptions<TContext> options);
public TContext CreateWithConnectionStringName(string connectionStringName, string migrationsAssemblyName)
{
var environmentName =
Environment.GetEnvironmentVariable(
"ASPNETCORE_ENVIRONMENT");
var basePath = AppContext.BaseDirectory;
return Create(basePath, environmentName, connectionStringName, migrationsAssemblyName);
}
private TContext Create(string basePath, string environmentName, string connectionStringName, string migrationsAssemblyName)
{
var builder = new ConfigurationBuilder()
.SetBasePath(basePath)
.AddJsonFile("appsettings.json")
.AddJsonFile($"appsettings.{environmentName}.json", true)
.AddEnvironmentVariables();
var config = builder.Build();
var connstr = config.GetConnectionString(connectionStringName);
Console.WriteLine($"Environment: {environmentName ?? "PRODUCTION"}");
if (string.IsNullOrWhiteSpace(connstr))
{
throw new InvalidOperationException(
$"Could not find a connection string named '{connectionStringName}'.");
}
else
{
return CreateWithConnectionString(connectionStringName, connstr, migrationsAssemblyName);
}
}
private TContext CreateWithConnectionString(string connectionStringName, string connectionString, string migrationsAssembly)
{
if (string.IsNullOrEmpty(connectionString))
throw new ArgumentException(
$"{nameof(connectionString)} is null or empty.",
nameof(connectionString));
var optionsBuilder =
new DbContextOptionsBuilder<TContext>();
Console.WriteLine(
"DesignTimeDbContextFactory.Create(string): Connection string: {0}",
connectionStringName);
optionsBuilder.UseSqlite(connectionString, db => db.MigrationsAssembly(migrationsAssembly));
DbContextOptions<TContext> options = optionsBuilder.Options;
return CreateNewInstance(options);
}
}
}
Resumindo o código desta classe, recebemos como parâmetro no construtor: connectionStringName e a migrationAssemblyName. Também lemos a variável de ambiente ASPNETCORE_ENVIRONMENT
que é a variável padrão que é utilizada para verificar em que ambiente estamos no .NET Core. Caso ela esteja preenchida com algo diferente de “Development” ele irá assumir que o ambiente é produtivo. Os arquivos appsettings.Development.json
e appsettings.json
são adicionados para que consiga ler os parâmetros de entrada e gerar as migrations.
Com isto implementado, apenas precisamos criar uma implementação concreta desta Factory, utilizando o DbContext
acima. Para isto, inclua a classe abaixo no ApplicationDbContext.cs
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using System.Reflection;
using DesignTimeExample.Models;
using Microsoft.EntityFrameworkCore;
namespace DesignTimeExample.Data
{
public class ApplicationContextDesignFactory : DesignTimeDbContextFactoryBase<ApplicationDbContext>
{
public ApplicationContextDesignFactory() : base("DefaultConnection", typeof(Startup).GetTypeInfo().Assembly.GetName().Name)
{ }
protected override ApplicationDbContext CreateNewInstance(DbContextOptions<ApplicationDbContext> options)
{
return new ApplicationDbContext(options);
}
}
}
Basicamente apenas estendemos a DesignTimeDbContextFactory
e passamos o nome da connection string que está nos arquivos de configuração.
Criação e atualização das migrations
Com tudo isto feito, já podemos testar a criação das migrations. Execute o comando a seguir para criar a versão inicial da estrutura do banco de dados:
1
dotnet ef migrations add application_v1 -o Data/Migrations
Com a migration inicial criada, vamos executa-la em ambiente de desenvolvimento. Verifique o conteúdo de sua variável de ambiente ASPNETCORE_ENVIRONMENT
.
Para ler a variável, no terminal use:
Linux/OSX
1
echo $ASPNETCORE_ENVIRONMENT
Windows
1
echo %ASPNETCORE_ENVIRONMENT%
Para atribuir a variável, no terminal use: Linux/OSX
1
export ASPNETCORE_ENVIRONMENT=Development
Windows
1
set ASPNETCORE_ENVIRONMENT=Development
Rode o comando a seguir com o ambiente configurado para developemnt:
1
dotnet ef database update
Perceba que foi gerado no diretório raíz de seu projeto, o arquivo application_dev.db
. Agora configure a variável de ambiente para production e rode o comando novamente:
1
dotnet ef database update
Agora o arquivo application_prod.db
foi gerado.
Conclusão
Com esta implementação é possível segregar os ambientes no momento de Design (execução dos comandos CLI), e nos ajuda a ter mais controle nos comandos que executamos, ou que mandamos para uma esteira executar no caso de estar rodando no modelo DevOps.
Até a próxima!