EF
Core 8 - Apresentando Complex Types
![]() |
Neste artigo vou apresentar o novo recurso Complex Types do EF Core 8. |
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:
C# - Tasks x Threads. Qual a diferença
DateTime - Macoratti.net
Null o que é isso ? - Macoratti.net
Formatação de data e hora para uma cultura ...
C# - Calculando a diferença entre duas datas
NET - Padrão de Projeto - Null Object Pattern
C# - Fundamentos : Definindo DateTime como Null ...
C# - Os tipos Nullable (Tipos Anuláveis)