EF Core 7 -  Mapeando para colunas JSON


 Neste artigo vou apresentar o mapeamento para colunas JSON, o novo recurso do EF Core 7.

A maioria dos bancos de dados relacionais oferece suporte a colunas que contêm documentos JSON. O JSON nessas colunas pode ser detalhado com consultas e isso permite, por exemplo, filtrar e ordenar pelos elementos dos documentos, bem como projetar elementos de fora dos documentos para resultados.

As colunas JSON permitem que os bancos de dados relacionais assumam algumas das características dos bancos de dados de documentos, criando um híbrido útil entre os dois.

O Entity Frameework Core 7 agora contém suporte independente de provedor para colunas JSON, com uma implementação para SQL Server. Esse suporte permite o mapeamento de agregações criadas a partir de tipos .NET para documentos JSON.

AS consultas LINQ normais podem ser usadas nas agregações e elas serão traduzidas para as construções de consulta apropriadas necessárias para detalhar o JSON. O EF 7 também oferece suporte à atualização e persistência de alterações nos documentos JSON.

Para mostrar este recurso na prática vamos criar uma aplicação Console no ambiente do .NET 7.0 usando o VS 2022 versão 17.4.0.

recursos usados:

Criando o projeto Console

Abra o VS 2022 e crie um novo projeto do tipo console chamado EFCJsonColumns.

Após criar o projeto inclua as referências aos pacotes :

A seguir crie duas pastas no projeto :  Entities e Data

Na pasta Entities crie as classes Aluno e Endereco:

1- Aluno

public class Aluno
{
  
public int Id { get; set; }
   [MaxLength(100)]
  
public string Nome { get; set; } = null!;
  
public Endereco Endereco { get; set; } = null!;
}

2- Endereco

//owned entity
public
class Endereco
{
  [MaxLength(200)]
 
public string Local { get; set; } = null!;
  [MaxLength(100)]
 
public string Pais { get; set; } = null!;
  [MaxLength(200)]
 
public string? Telefones { get; set; }
}

Temos aqui um cenário típico onde a entidade Endereco não possui um identificador e vai existir apenas como uma propriedade de navegação da entidade Aluno.  Aqui a entidade Endereco se assemelha a um aggregate, um padrão do DDD, que é um cluster de objetos de domínio que podem ser tratados como uma única unidade.

As entidades Aluno e Endereco são objetos separados mas é útil tratar Aluno junto com seus endereços como um único agregado. Para fazer isso vamos usar o recurso das owned entity type do EF Core definindo os tipos agregados usando o método OnwsOne.

Na pasta Data vamos criar a classe AppDbContext que é a classe de contexto onde definimos o mapeamento ORM.

1- AppDbContext

using EFCJsonColumns.Entities;
using
Microsoft.EntityFrameworkCore;
namespace
EFCJsonColumns.Data;

public class AppDbContext : DbContext
{
  
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
   {
     optionsBuilder.UseSqlServer(
      
"Data Source=.;Initial Catalog=Efc7DB;Integrated Security=True;TrustServerCertificate=True;")
       .LogTo(Console.WriteLine, Microsoft.Extensions.Logging.LogLevel.Information);
   }

   protected override void OnModelCreating(ModelBuilder modelBuilder)
   {
       modelBuilder.Entity<Aluno>().OwnsOne(x => x.Endereco, options =>
       {
           options.ToJson();
       });
   }

   public DbSet<Aluno> Alunos { get; set; }
}

Ao definir a string de conexão indicamos o nome do banco de dados a ser criado como Efc7DB e também definimos na string de conexão a propriedade TrustServerCertificate=True para ignorar os mecanismos normais de segurança.

Isso é necessário pois com o .NET 7.0 houve uma alteração no pacote Microsoft.Data.SqlClient e agora a string de conexão para o SqlClient utiliza o valor Encrypt=True e com isso o servidor precisa ser configurado com um certificado válido e o cliente precisa confiar neste certificado.

Se essas condições não forem atendidas teremos uma SqlException lançada quando a conexão for acionada.

Uma outra forma de mitigar o problema é definir explicitamente Encrypt=False na string de conexão.

Lembrando que ao adotar esses procedimentos em um ambiente de produção estamos colocando o servidor em um estado inseguro o que não é recomendável.

No método OnModelCreatingque é chamado pelo framework quando nosso contexto for criado pela primeira vez para construir o modelo e seu mapeamento na memória, vamos realizar o mapeamento usando o método OwnsOne para especificar que a propriedade Endereco é uma Owned Entity da entidade Aluno.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Aluno>().OwnsOne(x => x.Endereco);
}

Com isso teremos a criação de uma única tabela contendo 3 novas colunas relacionadas com as propriedades da entidade Endereco. 

Entretanto não é isso que queremos obter queremos realizar o mapeamento para uma coluna JSON de forma a obter uma única tabela com apenas uma única nova coluna representando o endereço que será salvo no formato JSON.

Para fazer isso usamos o método ToJson na entidade Aluno, assim todas as entidades pertencentes a Aluno serão mapeadas automaticamente para mesma coluna JSON.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Aluno>().OwnsOne(x => x.Endereco, options =>
    {
         options.ToJson();
    });
}

Com isso teremos a criação de uma única tabela contendo uma nova coluna onde os dados de Endereco serão salvos no formato JSON.

Aplicando o Migrations usando os comandos :

Teremos a criação do banco de dados Efc7DB e da tabela Alunos com a seguinte estrutura:

Vamos agora incluir alguns dados na tabela Alunos criando na pasta Data a classe PopulaDatabase e o método estático PopularTabela() :

using EFCJsonColumns.Entities;
using Microsoft.EntityFrameworkCore;
namespace EFCJsonColumns.Data;
public class PopulaDatabase
{
    public static void PopularTabela()
    {
        using (var context = new AppDbContext())
        {
            context.Database.ExecuteSqlRaw("truncate table Alunos");
            var maria = new Aluno()
            {
                Nome = "Maria",
                Endereco = new Endereco
                {
                    Pais = "Brasil",
                    Local = "Rua Projetada 100",
                    Telefones = "12-9854-7777,13-9854-6666"
                }
            };
            var amanda = new Aluno()
            {
                Nome = "Amanda",
                Endereco = new()
                {
                    Local = "Pça XV de Novembro 20",
                    Pais = "Brasil",
                    Telefones = "11-8875-1234"
                }
            };
            var manoel = new Aluno()
            {
                Nome = "Manoel",
                Endereco = new()
                {
                    Local = "Rua Camões 34",
                    Pais = "Portugal",
                    Telefones = "55 7605-1234"
                }
            };
            var cristina = new Aluno()
            {
                Nome = "Cristina",
                Endereco = new()
                {
                    Local = "Calle Felipe Vallese 10",
                    Pais = "Argentina",
                    Telefones = "55 213-215-200"
                }
            };
            context.Add(maria);
            context.Add(amanda);
            context.Add(manoel);
            context.Add(cristina);
            context.SaveChanges();
        }
    }
}

Na classe Program vamos chamar este método e a seguir vamos examinar os dados na tabela Alunos :

PopulaDatabase.PopularTabela();

Abrindo o SQL Server Management Studio podemos verificar a tabela Alunos:

Para consultar os dados podemos usar uma consulta LINQ padrão usando o seguinte código :

 using (var context = new AppDbContext())
 {
        var agenda = context.Alunos
            .Where(p => p.Endereco.Pais == "Brasil")
            .OrderBy(p => p.Endereco.Local)
            .ToList();
        Console.WriteLine();
        foreach (var a in agenda)
        {
            Console.WriteLine($"{a.Nome} - {a.Endereco.Pais} - " +
                              $"{a.Endereco.Local} : {a.Endereco.Telefones}");
        }
    }

Executando o código teremos o resultado abaixo:

Note que é utilizando o comando JSON_VALUE para extrair um valor escalar (números,strings, etc.) de uma coluna JSON existente.

Podemos também atualizar uma informação na coluna Endereco. Vamos alterar o endereço do aluno para o país argentina :

 using (var context = new AppDbContext())
 {
        var aluno = context.Alunos
                           .FirstOrDefault(a => a.Endereco.Pais == "Argentina");
        aluno.Endereco.Local = "Calle Corrientes 999";
        context.SaveChanges();
 }

Executando este código será gerada a seguinte consulta pelo EF Core :

Podemos ver o uso do comando JSON_MODIFY que é acionado quando apenas um subdocumento for alterado.

Com isso temos um recurso importante incorporado ao Entity Framework mesmo que ainda não totalmente completo visto que embora o suporte a JSON no EF7 estabeleça as bases para o suporte de colunas JSON entre provedores completos em versões futuras ainda faltam implementar recursos como :

E estamos conversados...

Pegue o projeto aqui :   EFCJsonColumns.zip

"De tudo o que se tem ouvido, o fim é: Teme a Deus, e guarda os seus mandamentos; porque isto é o dever de todo o homem."
Eclesiastes 12:13

Referências:


José Carlos Macoratti