Novidades do EF Core 2.1 - Conversão entre enumerados e strings


Recentemente a Microsoft anunciou a entrada em produção da versão 2.1 para suas principais tecnologias: .NET Core, Asp.NET Core e Entity Framework Core.
No caso do Entity algumas coisas bem interessantes foram disponibilizadas, como por exemplo a possibilidade de informar como o valor de um enumerado será convertido no banco de dados. Até a versão anterior (2.0.3), o EF Core não suportava essa conversão.
Nesse artigo quero mostrar essa limitação e como a versão 2.1 o supera. Para isso vou usar um projeto de cadastro da eficiência energética de equipamentos eletrodomésticos. Vamos lá?
Enfrentei essa limitação em meu curso na Alura sobre o uso do Entity Framework Core em bancos legados. Nele mostrei uma alternativa para superá-la. Se você tem acesso a Alura, mostrei esse tópico nessa aula aqui.
O problema em questão
Considere um banco de dados já existente com uma tabela que registra a eficiência energética de equipamentos eletrodomésticos. O script de criação desse banco é o seguinte:
CREATE TABLE [dbo].[EficienciaEnergetica] (
[Id] INT IDENTITY (1, 1) NOT NULL,
[TipoEquipamento] NVARCHAR (50) NOT NULL,
[Fabricante] NVARCHAR (50) NOT NULL,
[Modelo] NVARCHAR (50) NULL,
[Eficiencia] CHAR (1) NOT NULL,
[ConsumoMensal] DECIMAL (19, 3) NULL,
PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [CHK_EficienciaEnergetica] CHECK ([Eficiencia]='A' OR [Eficiencia]='B'
OR [Eficiencia]='C' OR [Eficiencia]='D' OR [Eficiencia]='E' OR [Eficiencia]='F'
OR [Eficiencia]='G')
);
Repare que existe uma check constraint na tabela. Ela restringe o valor que pode ser armazenado na coluna Eficiencia para os valores A até G.
Poderíamos criar em nosso programa uma classe que fosse mapeada nessa tabela assim:
class SeloProcel
{
public int Id { get; set; }
public string TipoEquipamento { get; set; }
public string Fabricante { get; set; }
public string Modelo { get; set; }
public string Eficiencia { get; set; }
public decimal ConsumoMensal { get; set; }
}
E para incluir alguns registros nessa tabela usando o EF Core eu usaria o seguinte código:
var geladeira1 = new SeloProcel
{
TipoEquipamento = "Geladeira",
Fabricante = "Brastemp",
Modelo = "BCF07A",
Eficiencia = "A",
ConsumoMensal = 10.39M,
};
var geladeira2 = new SeloProcel
{
TipoEquipamento = "Geladeira",
Fabricante = "Electrolux",
Modelo = "AE12F",
Eficiencia = "D",
ConsumoMensal = 8.5M
};
using (var ctx = new AppDbContext())
{
ctx.EficienciaEnergetica.AddRange(geladeira1, geladeira2);
ctx.SaveChanges();
}
A classe de contexto está listada abaixo:
class AppDbContext : DbContext
{
public DbSet EficienciaEnergetica { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseSqlServer("CONNECTION STRING AQUI...");
}
}
Repare que começamos mapeando a propriedade Eficiencia, que é do tipo string, ao tipo nvarchar. Tudo bem até aqui. O problema é que, do jeito que a classe está mapeada, permitimos um código assim:
var geladeira3 = new SeloProcel
{
TipoEquipamento = "Geladeira",
Fabricante = "Electrolux",
Modelo = "AE12F",
Eficiencia = "X",
ConsumoMensal = 8.5M
};
Quando tentássemos incluir esse objeto no banco seríamos presenteados com o erro:
The INSERT statement conflicted with the CHECK constraint "CHK_Eficiencia".
Isso aconteceu porque a check constraint limitou o valor X que foi colocado na propriedade Eficiencia do objeto.
Perfeito, a constraint funcionou! Mas como fazer com que os consumidores da classe SeloProcel não cometam equívocos ao definir valores para essa propriedade? Ora, se queremos permitir apenas alguns valores finitos uma boa opção será utilizar enumerados. Vamos experimentar essa idéia.

Usando enumerados
Vamos mudar nossa classe principal para utilizar enumerados na propriedade Eficiencia. Para isso criamos o enumerado EficienciaEnergetica
.
enum EficienciaEnergetica
{
A, B, C, D, E, F, G
}
class SeloProcel
{
public int Id { get; set; }
public string TipoEquipamento { get; set; }
public string Fabricante { get; set; }
public string Modelo { get; set; }
public EficienciaEnergetica Eficiencia { get; set; }
public decimal ConsumoMensal { get; set; }
}
Legal! Agora o código usado para inserir a terceira geladeira nem compila! Alterando esse código para um válido...
var geladeira3 = new SeloProcel
{
TipoEquipamento = "Geladeira",
Fabricante = "Electrolux",
Modelo = "AE12F",
Tensao = Tensao.Tensao127V,
Eficiencia = EficienciaEnergetica.B,
ConsumoMensal = 8.5M
};
... posso em seguida persistir a geladeira no banco.
Contudo, ao executar o código continuo recebendo o mesmo erro de check constraint. Porquê? O que aconteceu?
A limitação do EF Core 2 para converter enumerados e como superá-la
Quando vai persistir alguma propriedade do tipo enumerado o EF Core por convenção utiliza o inteiro referente ao valor do enum.
Por exemplo, para o valor EficienciaEnergetica.F
ele vai usar o inteiro 5 (porque esse valor é o quinto elemento no enumerado). Por isso a check constraint impede a inclusão: o valor 5 não está entre os permitidos.
O problema é que a versão 2 do EF Core não oferecia possibilidade de alterar esse comportamento! O que fazer então?
Para superar essa limitação eu vou seguir a indicação descrita nesta issue, registrada no repositório do EF Core no Github.
A solução que usei se dividia em 2 partes:
- Escrever uma classe de conversão usando métodos de extensão e um dicionário com o mapeamento
- Criar duas propriedades na entidade, uma do tipo string para ser persistida e outra do tipo enumerado para ser consumida no código
A classe de conversão
A classe de conversão está listada abaixo. Ela utiliza dois métodos de extensão e um dicionário para fazer o de-para.
static class EficienciaEnergeticaExtensions
{
private static Dictionary<string, EficienciaEnergetica> mapa =
new Dictionary<string, EficienciaEnergetica>
{
{ "A", EficienciaEnergetica.A },
{ "B", EficienciaEnergetica.B },
{ "C", EficienciaEnergetica.C },
{ "D", EficienciaEnergetica.D },
{ "E", EficienciaEnergetica.E },
{ "F", EficienciaEnergetica.F },
{ "G", EficienciaEnergetica.G },
};
static string ParaString(this EficienciaEnergetica ee)
{
return mapa.First(e => e.Value == ee).Key;
}
static EficienciaEnergetica ParaEnum(this string str)
{
return mapa[str];
}
}
Para converter do enumerado para string fazemos EficienciaEnergetica.B.ParaString()
e para converter de string para enumerado "A".ParaEnum()
(repare que se tentarmos converter uma string não-mapeada uma exceção será lançada).
Mapeando as propriedades no EF Core 2.0
Já a entidade precisa ser modificada para se adequar a essa limitação. Podemos começar abandonando o recurso de propriedades auto-implementadas em favor de um campo privado, onde faríamos o de-para nos métodos de acesso para leitura/escrita:
private string _eficiencia;
public EficienciaEnergetica Eficiencia {
get => _eficiencia.ParaEnum();
set => _eficiencia = value.ParaString();
}
Porém, o EF Core não suporta o mapeamento de campos privados! Uma pena, já que essa solução utiliza uma prática comum em orientação a objetos: o encapsulamento com um campo privado. O que fazer então?
Vamos tornar o campo privado uma propriedade pública com escrita privada. Por convenção o nome de propriedades começa com maiúsculas, então mudamos seu nome para EficienciaString
(você com certeza vai pensar em um nome mais adequado que o meu!).
public string EficienciaString { get; private set; }
public EficienciaEnergetica Eficiencia {
get => EficienciaString.ParaEnum();
set => EficienciaString = value.ParaString();
}
Pronto. Agora podemos configurar o mapeamento adequado no EF Core. Primeiro iremos informar que é a propriedade EficienciaString
que será mapeada no banco com a coluna Eficiencia
e depois vamos pedir que o entity ignore a propriedade Eficiencia
. Veja o código:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity(options => {
options.Property(e => e.EficienciaString)
.HasColumnName("Eficiencia");
options.Ignore(e => e.Eficiencia);
});
}
Agora se eu executar o código anterior para incluir a terceira geladeira eu terei sucesso, porque o EF está convertendo a propriedade para a string específica (B no caso) e a check constraint irá validar esse valor. Ótimo!
O único senão é o fato de expormos uma propriedade (EficienciaString) apenas para superar uma limitação em uma ferramenta. Um desenvolvedor que consumisse essa classe poderia ficar confuso ao ver duas propriedades relacionadas à eficiência. Qual delas utilizar?
Mas com a atualização do EF Core para a versão 2.1 isso não é mais necessário!! Agora o EF Core suporta a conversão de valores entre propriedades do modelo e colunas do banco. Vamos ver como isso é feito e substituir a solução acima.
Como EF Core 2.1 converte valores
Segundo a documentação do EF Core 2.1 sobre o assunto, agora é possível converter valores de propriedades ao ler e escrever na base de dados. Isso pode ser feito para converter valores do mesmo tipo ou tipos diferentes. Um exemplo da segunda alternativa é a conversão de um tipo enumerado em uma string.
Para realizar a conversão usamos o método HasConversion()
no momento da configuração do modelo. Esse método faz parte da classe PropertyBuilder
e tem 8 sobrecargas, cada uma com uma alternativas de conversão. Meu objetivo nesse artigo não é explorar todas as possibilidades. Se ficou curioso, dê uma lida na documentação através do link que disponibilizei.
A sobrecarga que vamos utilizar é uma que recebe dois delegates Func como entrada. Um para converter do enumerado para string e o outro na direção contrária. Vamos lá?
Aplicando a nova solução
Primeiro vamos atualizar o EF Core para a versão 2.1. No Nuget Manager para o projeto, clico na aba Installed e depois em Update (certifique-se que está atualizando o EF Core para uma versão 2.1 ou superior).
Em seguida removo a propriedade EficienciaString
e volto a propriedade Eficiencia
para uma propriedade auto-implementada:
public EficienciaEnergetica Eficiencia { get; set; }
Agora vou na classe de contexto e substituo o código anterior pelo código de conversão...
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity(options => {
options.Property(e => e.Eficiencia)
.HasConversion(
e => e.ParaString(),
s => s.ParaEnum()
);
});
}
Beleza! Testo a aplicação com uma inclusão de ar condicionado e tudo funciona perfeitamente.
Conclusão
É sempre importante buscar um código simples e conciso. Algumas vezes isso não é possível por conta de limitações nas tecnologias que usamos. Mas sempre que possível devemos revisitar nosso código para verificar se podemos refatorá-lo. Com a nova versão do EF Core 2.1, pudemos voltar ao design simples de um POCO (Plain Old CSharp Object), sem abiguidades ou dúvidas para os desenvolvedores que irão consumir a classe SeloProcel.
Também vale a pena ficar antenado nas atualizações das tecnologias que você utiliza em seus projetos, principalmente quando o time de desenvolvimento já houver sinalizado que iria implementar a solução dessas limitações em seu roadmap.
Se desejar baixar e conferir o código que utilizei nesse artigo, vá até seu repositório no Github.
Até o próximo artigo!