EF Core 8 - Apresentando Complex Types


 Neste artigo vou apresentar o novo recurso Complex Types do EF Core 8.
 
O EF Core 8 introduz os tipos complexos permitindo com isso a implementação de objetos de valor de forma mais eficiente e estruturada.

No EF Core 8 o uso de tipos complexos como objetos de valor resolve o problema de exceções em tempo de execução ao usar entidades de propriedade (owned entity)

Ao contrário das entidades de propriedade, a mesma instância de tipo complexo pode ser reutilizada várias vezes na mesma entidade ou em entidades diferentes

O EF Core 8 introduz também o uso de records posicionais para objetos de valor, permitindo uma maneira mais concisa e eficiente de definir e atualizar propriedades

Os tipos complexos no EF Core 8 permitem projeções, oferecendo assim recursos de consulta mais avançados.

Essas são algumas das novidades e recursos relacionados com os Tipos Complexos.

O que é um Complex Type

Um Complex Type é um tipo de objeto que pode ser usado no EF Core 8 para representar dados complexos que não precisam de uma identidade própria. (Value Objects)

Eles são semelhantes aos Owned Types, mas com algumas diferenças :

Os Complex Types podem ser usados para representar uma variedade de dados complexos, como: Endereços, Pessoas, Produtos, Ordens de compra, etc.

Complex Type e Value Object

Assim, o conceito de Complex type esta relacionado com o conceito de Value Object que por sua vez está diretamente relacionado ao Domain Driven Design (DDD).  Eles são usados para representar conceitos do domínio do negócio, como cores, tamanhos, preços, etc.

Um Value Object (Objeto de Valor) é um tipo de objeto que representa um valor sem uma identidade própria sendo definido apenas pelos seus atributos e não possui uma identificação única. (Id)

Os Value Objects são frequentemente utilizados para modelar conceitos imutáveis e sem vida própria, como números complexos, intervalos de tempo, coordenadas geográficas, entre outros.

Características dos Value Objects:

Em um cenário onde temos uma classe Cliente com as propriedades ClienteId, Nome e Endereco :

public class Cliente
{
   public int ClienteId { get; set; }
   public required string Nome { get; set; }
   public required Endereco Endereco { get; set; }
}

Temos que cada cliente deve ser identificado pelo seu Id ,  assim Cliente é uma classe sendo definida como uma entidade e possui um identificador único.

No entanto Endereco pode ser representado como uma classe sem possuir um identificador único pois isso é feito pelas suas propriedades :

public class Endereco
{
   public required string Cep { get; set; }
   public required string Local { get; set; }
   public required string Cidade { get; set; }
  public required string Estado { get; set; }
}

Visto que não faz sentido Endereco existir sem um cliente e assim ela esta vinculado ao cliente. Assim a classe Endereco não possui um identificador unico.

Definindo o relacionamento usando Owned type

Até o NET 7 podíamos implementar o relacionamento entre Cliente e Endereco no EF Core usando owned types ou tipos de propriedade.  Isso é feito no código usando o método OwnsOne no método OnModelCreating :

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

Ao usar este recurso teremos uma relação um-para-um entre Cliente e Endereco e uma chave primária é criada como uma propriedade de sombra para o tipo de propriedade.

Uma limitação desta abordagem é que não podemos reutilizar a mesma instancia do tipo de propriedade varias vezes na mesma entidade ou em entidades diferentes alem de outras limitações

Para resolver isso agora podemos usar o novo recurso Complex Type.

Configurando o Complex Type

Para configurar um Complex Type no EF Core 8 temos duas opções:

1- Usar o atributo Data Annotation [ComplexType]

Adicionar o atributo Data Annotation [ComplexType] para especificar um tipo de referência ou um tipo de valor deve ser tratado como um tipo complexo e marcar a navegação complexa de propriedades como obrigatória(required):

[ComplexType]
public class Endereco
{
   public required string Cep { get; set; }
   public required string Local { get; set; }
   public required string Cidade { get; set; }
   public required string Estado { get; set; }
}

2- Usar a Fluent API

Usar a configuração ComplexProperty com o método IsRequired no método OnModelCreating para especificar um tipo de referência ou um tipo de valor que deve ser tratado como um tipo complexo.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
   modelBuilder.Entity<Cliente>().ComplexProperty(a => a.Endereco, a=> { a.IsRequired();});
}

Aplicando o conceito na prática

Vamos criar um projeto Console no  VS 2022 chamado EFCore8_ComplexType onde vamos incluir os seguintes pacotes nuget:

E a seguir definir uma pastas Entities contendo as classes Endereco e Cliente:

1- Cliente

public class Cliente
{
   public int ClienteId { get; set; }
   public required string Nome { get; set; }
   public required Endereco Endereco { get; set; }
}

2-Endereco

public class Endereco
{
   public required string Cep { get; set; }
   public required string Local { get; set; }
   public required string Cidade { get; set; }
  public required string Estado { get; set; }
}

Vamos criar no projeto a classe AppDbContext onde vamos definir a

public class AppDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer("string_conexao;");
        optionsBuilder.LogTo(Console.WriteLine, LogLevel.Information);
        base.OnConfiguring(optionsBuilder);
    }
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
         modelBuilder.Entity<Cliente>().ComplexProperty(a => a.Endereco, 
                                                                                   a=> { a.IsRequired();});
    }
    public DbSet<Cliente> Clientes { get; set; }}

 

A seguir na classe Program vamos incluir o código abaixo:

using var _context = new AppDbContext();
var cliente1 = new Cliente
{
    Nome = "Maria",
    Endereco = new Endereco
    {
        Cep = "1024500",
        Local = "Rua Projetada,100",
        Cidade = "Lins",
        Estado = "SP"
    }
}
_context.Clientes.Add(cliente1);
_context.SaveChanges();
Console.ReadKey();

1- Vamos executar e ver a consulta SQL Gerada pelo  EF core com base na configuração do tipo complexa que foi usada na Fluent Api:

SET IMPLICIT_TRANSACTIONS OFF;
SET NOCOUNT ON;
INSERT INTO [Clientes] ([Nome], [Endereco_Cep], [Endereco_Cidade], [Endereco_Estado], [Endereco_Local])
OUTPUT INSERTED.[ClienteId]
VALUES (@p0, @p1, @p2, @p3, @p4);


Note que a instrução insert into esta incluindo o cliente e o endereço em uma única consulta SQL  na mesma tabela e que  temos as colunas mapeadas do cliente e do endereço na mesma tabela.

2- Vejamos agora outro exemplo onde vamos criar mais uma instancia de cliente e atribuir a mesma instancia de endereco :

using var _context = new AppDbContext();
var endereco = new Endereco
{
    Cep = "1024500",
    Local = "Rua Projetada,100",
    Cidade = "Lins",
    Estado = "SP"
};
var cliente1 = new Cliente
{
    Nome = "Maria",
    Endereco = endereco
};
var cliente2 = new Cliente
{
    Nome = "Pedro",
    Endereco = endereco
};
_context.Clientes.Add(cliente1);
_context.Clientes.Add(cliente2);

_context.SaveChanges();

Executando este código deveremos ter ambos os clientes sendo incluídos na mesma consulta conforme mostra a consulta SQL gerada:

INSERT ([Nome], [Endereco_Cep], [Endereco_Cidade], [Endereco_Estado], [Endereco_Local])
VALUES (i.[Nome], i.[Endereco_Cep], i.[Endereco_Cidade], i.[Endereco_Estado], i.[Endereco_Local])
OUTPUT INSERTED.[ClienteId], i._Position;

Observe que o EF Core esta utilizando uma instrução MERGE condicional e usando a instrução Insert Into
incluindo na mesma consulta ambos os clientes com o mesmo endereco

Se tentássemos usar a abordagem anterior usando os tipos de propriedade teríamos obtido uma exceção
em tempo de execução isso ocorre pois os tipos de propriedades são na verdade entidades que possuem uma chave primaria oculta  e por isso não podem usar a mesma instancia da entidade para mais de um objeto o que não ocorre com os tipos complexos que são tratados como value objects

3- Vejamos agora como atualizar uma instância de endereco usando o código abaixo:

using var _context = new AppDbContext();

var endereco = new Endereco
{
Cep = "1024500",
Local = "Rua Projetada,100",
Cidade = "Lins",
Estado = "SP"
};

var cliente1 = new Cliente
{
Nome = "Maria",
Endereco = endereco
};

var cliente2 = new Cliente
{
Nome = "Pedro",
Endereco = endereco
};

_context.Clientes.Add(cliente1);
_context.Clientes.Add(cliente2);
_context.SaveChanges();

endereco.Local = "Rua Projetada 200";
_context.SaveChanges();

Console.ReadKey();

Executando teremos a seguinte consulta SQL Gerada

WHEN NOT MATCHED THEN
INSERT ([Nome], [Endereco_Cep], [Endereco_Cidade], [Endereco_Estado], [Endereco_Local])
VALUES (i.[Nome], i.[Endereco_Cep], i.[Endereco_Cidade], i.[Endereco_Estado], i.[Endereco_Local])
OUTPUT INSERTED.[ClienteId], i._Position;
info: 28/12/2023 09:29:37.187 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (2ms) [Parameters=[@p1='?' (DbType = Int32), @p0='?' (Size = 4000), @p3='?' (DbType = Int32), @p2='?' (Size = 4000)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
UPDATE [Clientes] SET [Endereco_Local] = @p0
OUTPUT 1
WHERE [ClienteId] = @p1;
UPDATE [Clientes] SET [Endereco_Local] = @p2
OUTPUT 1
WHERE [ClienteId] = @p3;


Observe que temos a mesma instrução MERGE que inclui ambos os clientes junto com os endereços e  note que temos outro comando SQL Update que atualiza os clientes e define o novo local do endereço

Assim quando atualizamos o valor de uma propriedade em um tipo complexo ela será atualiza nas entidades relacionadas  pois estamos compartilhando a mesma instancia  para ambas as entidades cliente e temos assim uma instrução Update para cada um dos clientes.

3- Agora vejamos o uso de record posicionais

Podemos definir a classe Endereco como um record :

public record Endereco(string Cep,string Local, string, string Cidade, string Estado);

Vamos atualizar o construtor usado :

var endereco = new Endereco("1024500","Rua Projetada 100","Lins","SP");

e temos que ajustar o codigo para atualizar o endereco de um cliente usando a notação dos records :

cliente1.Endereco = cliente1.Endereco with { Local = "Rua Projetada 300" };

Assim teremos agora o Update apenas para o cliente1:

INSERT ([Nome], [Endereco_Cep], [Endereco_Cidade], [Endereco_Estado], [Endereco_Local])
VALUES (i.[Nome], i.[Endereco_Cep], i.[Endereco_Cidade], i.[Endereco_Estado], i.[Endereco_Local])
OUTPUT INSERTED.[ClienteId], i._Position;
info: 28/12/2023 09:43:53.439 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (2ms) [Parameters=[@p1='?' (DbType = Int32), @p0='?' (Size = 4000)], CommandType='Text', CommandTimeout='30']
SET IMPLICIT_TRANSACTIONS OFF;
SET NOCOUNT ON;
UPDATE [Clientes] SET [Endereco_Local] = @p0
OUTPUT 1
WHERE [ClienteId] = @p1;

4- Para concluir vejamos como realizar projeções usando os tipos complexos.

Vamos iniciar criando uma consulta para obter o primeiro endereço de um cliente:

var endereco =_context.Clientes.Select(e=> e.Endereco).First();

 Executando temos a seguinte consulta SQL Gerada :

Executed DbCommand (12ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT TOP(1) [c].[Endereco_Cep], [c].[Endereco_Cidade], [c].[Endereco_Estado], [c].[Endereco_Local]
FROM [Clientes] AS [c]


Aqui temos a instrução Select obtendo as colunas para o tipo endereco para o primeiro cliente,  e como os tipos complexos não são rastreados pelo EF Core temos um ótimo desempenho.

Vejamos outra consulta para obter os clientes cujo endereço estão em SP:

var clientes = _context.Clientes.Where(e => e.Endereco.Estado == "SP").ToList();

Temos a seguinte consulta gerada :

FROM [Clientes] AS [c]
WHERE [c].[Endereco_Estado] = N'SP'


Observe que temos a instrução Where referenciando a coluna Endereco_Estado corretamente assim usando complex type temos a opção de realizar projeções de forma simples e com diversas opções

Com isso vimos que os Complex Types são uma nova funcionalidade do EF Core 8 que fornece uma maneira eficiente de representar dados complexos e podem ser usados para simplificar o seu código e melhorar o desempenho do seu aplicativo.

Pegue o código do projeto aqui:  https://github.com/macoratti/EFCore8_ComplexType

"A ti clamarei, ó Senhor, Rocha minha; não emudeças para comigo; não aconteça, calando-te tu para comigo, que eu fique semelhante aos que descem ao abismo"
Salmos 28:1

Referências:


José Carlos Macoratti