Novidades do .NET 10 – Suporte Nativo ao tipo JSON no EF Core 10
O Entity Framework Core 10 traz uma das melhorias mais significativas para quem trabalha com dados semi-estruturados: o suporte nativo ao tipo de dados json do SQL Server 2025 e Azure SQL Database. Essa mudança elimina a necessidade de armazenar JSON em colunas nvarchar(max), trazendo ganhos reais de performance, validação e eficiência de armazenamento.
Acesse os artigos e aprofunde seus conhecimentos sobre as melhorias que o .NET 10 oferece para o desenvolvimento de aplicações mais rápidas, seguras e eficientes:
O problema: JSON em colunas de texto
Antes do EF Core 10, o suporte a JSON no SQL Server funcionava, mas com limitações importantes. O JSON era armazenado em colunas nvarchar(max) — essencialmente texto puro. Isso significava:
- Sem validação nativa: O banco de dados aceitava qualquer string, válida ou não como JSON
- Overhead de parsing: Cada consulta precisava interpretar o texto como JSON
- Armazenamento ineficiente: Texto ocupa mais espaço que formatos binários otimizados
- Indexação limitada: Índices em colunas JSON exigiam colunas computadas ou índices full-text
O EF Core 7 e 8 introduziram mapeamento JSON via ToJson(), mas a coluna subjacente continuava sendo nvarchar(max):
|
1 2 3 4 5 6 7 |
<em>// EF Core 7/8/9 - JSON armazenado como nvarchar(max)</em> protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Pedido>() .OwnsOne(p => p.Detalhes, b => b.ToJson()); } |
SQL gerado (EF Core 9 e anteriores):
|
1 2 3 4 5 6 7 |
CREATE TABLE [Pedidos] ( [Id] int NOT NULL IDENTITY, [Numero] nvarchar(max) NOT NULL, [Detalhes] nvarchar(max) NOT NULL, <em>-- JSON como texto</em> CONSTRAINT [PK_Pedidos] PRIMARY KEY ([Id]) ); |
A solução: tipo nativo json no EF Core 10
O SQL Server 2025 e o Azure SQL Database introduziram o tipo de dados json nativo. O EF Core 10 aproveita esse recurso automaticamente, oferecendo:
- Validação garantida: O banco rejeita JSON inválido na inserção
- Armazenamento otimizado: Formato binário interno mais compacto
- Queries mais rápidas: Sem necessidade de parsing repetido
- Funções nativas aprimoradas:
JSON_VALUE() RETURNING,modify()e outras
Requisitos
Para usar o tipo json nativo, você precisa de:
- SQL Server 2025 ou Azure SQL Database
- EF Core 10 configurado com:
UseAzureSql(), ou- Nível de compatibilidade 170 ou superior
Como habilitar
Opção 1: Azure SQL Database
|
1 2 3 4 5 6 |
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseAzureSql( "Server=seu-servidor.database.windows.net;Database=MinhaBase;..."); } |
Opção 2: SQL Server 2025 com compatibilidade 170
|
1 2 3 4 5 6 7 |
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer( "Server=localhost;Database=MinhaBase;...", o => o.UseCompatibilityLevel(170)); } |
Com qualquer uma dessas configurações, o EF Core 10 automaticamente usa o tipo json para:
- Coleções primitivas (
string[],List<int>, etc.) - Tipos complexos mapeados com
ToJson()
Exemplo prático: modelagem com tipos complexos
Definindo as classes
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
public class Pedido { public int Id { get; set; } public string Numero { get; set; } = string.Empty; public DateTime DataCriacao { get; set; } public string[] Tags { get; set; } = []; public required DetalhesPedido Detalhes { get; set; } public List<ItemPedido> Itens { get; set; } = []; } public class DetalhesPedido { public string? Observacao { get; set; } public decimal Desconto { get; set; } public int Prioridade { get; set; } } public class ItemPedido { public string Produto { get; set; } = string.Empty; public int Quantidade { get; set; } public decimal PrecoUnitario { get; set; } } |
Configurando o DbContext
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public class LojaDbContext : DbContext { public DbSet<Pedido> Pedidos => Set<Pedido>(); protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseAzureSql( "Server=seu-servidor.database.windows.net;Database=Loja;..."); } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Pedido>(entity => { <em>// Mapeia DetalhesPedido como JSON</em> entity.ComplexProperty(p => p.Detalhes, b => b.ToJson()); <em>// Mapeia coleção de itens como JSON</em> entity.OwnsMany(p => p.Itens, b => b.ToJson()); }); } } |
SQL gerado na migração
|
1 2 3 4 5 6 7 8 9 10 |
CREATE TABLE [Pedidos] ( [Id] int NOT NULL IDENTITY, [Numero] nvarchar(max) NOT NULL, [DataCriacao] datetime2 NOT NULL, [Tags] json NOT NULL, [Detalhes] json NOT NULL, [Itens] json NOT NULL, CONSTRAINT [PK_Pedidos] PRIMARY KEY ([Id]) ); |
Observe que Tags, Detalhes e Itens agora são do tipo json, não nvarchar(max).
Consultando propriedades JSON com LINQ
O EF Core 10 traduz acesso a propriedades JSON diretamente para funções SQL nativas:
Filtrar por propriedade dentro do JSON
|
1 2 3 4 5 |
<em>// Buscar pedidos com prioridade alta</em> var pedidosUrgentes = await context.Pedidos .Where(p => p.Detalhes.Prioridade > 5) .ToListAsync(); |
SQL gerado:
|
1 2 3 4 |
SELECT [p].[Id], [p].[Numero], [p].[DataCriacao], [p].[Tags], [p].[Detalhes], [p].[Itens] FROM [Pedidos] AS [p] WHERE JSON_VALUE([p].[Detalhes], '$.Prioridade' RETURNING int) > 5 |
O EF Core 10 usa a nova sintaxe JSON_VALUE() RETURNING do SQL Server 2025, que especifica o tipo de retorno diretamente na função SQL.
Ordenar por propriedade JSON
|
1 2 3 4 5 6 |
<em>// Ordenar por desconto (maior primeiro)</em> var pedidosOrdenados = await context.Pedidos .OrderByDescending(p => p.Detalhes.Desconto) .Take(10) .ToListAsync(); |
Filtrar por texto dentro do JSON
|
1 2 3 4 5 6 |
<em>// Buscar pedidos com observação específica</em> var pedidosComObs = await context.Pedidos .Where(p => p.Detalhes.Observacao != null && p.Detalhes.Observacao.Contains("urgente")) .ToListAsync(); |
ExecuteUpdate com JSON: atualizações eficientes
Uma das maiores melhorias do EF Core 10 é o suporte completo a ExecuteUpdate com propriedades JSON. Isso permite atualizar campos dentro do JSON sem carregar a entidade na memória.
Incrementar um campo numérico
|
1 2 3 4 5 6 |
<em>// Aumentar prioridade de todos os pedidos do dia</em> await context.Pedidos .Where(p => p.DataCriacao.Date == DateTime.Today) .ExecuteUpdateAsync(s => s.SetProperty(p => p.Detalhes.Prioridade, p => p.Detalhes.Prioridade + 1)); |
SQL gerado (SQL Server 2025):
|
1 2 3 4 5 6 |
UPDATE [p] SET [Detalhes] = JSON_MODIFY([p].[Detalhes], '$.Prioridade', JSON_VALUE([p].[Detalhes], '$.Prioridade' RETURNING int) + 1) FROM [Pedidos] AS [p] WHERE CAST([p].[DataCriacao] AS date) = CAST(GETDATE() AS date) |
O SQL Server 2025 usa a função JSON_MODIFY() para atualizações parciais, evitando reescrever o documento JSON inteiro.
Atualizar texto dentro do JSON
|
1 2 3 4 5 6 7 |
<em>// Adicionar prefixo à observação</em> await context.Pedidos .Where(p => p.Detalhes.Observacao != null) .ExecuteUpdateAsync(s => s.SetProperty(p => p.Detalhes.Observacao, p => "[PROCESSADO] " + p.Detalhes.Observacao)); |
Zerar um campo
|
1 2 3 4 5 6 |
<em>// Remover desconto de pedidos antigos</em> await context.Pedidos .Where(p => p.DataCriacao < DateTime.Today.AddDays(-30)) .ExecuteUpdateAsync(s => s.SetProperty(p => p.Detalhes.Desconto, 0m)); |
Migração automática: nvarchar para json
Se você já tem uma aplicação EF Core que armazena JSON em colunas nvarchar(max), o EF Core 10 converte automaticamente para o tipo json na primeira migração — desde que o nível de compatibilidade 170 esteja configurado.
O que acontece na migração
Ao rodar dotnet ef migrations add AtualizarParaJson, o EF Core gera:
|
1 2 3 4 5 6 7 8 9 10 11 |
protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.AlterColumn<string>( name: "Detalhes", table: "Pedidos", type: "json", nullable: false, oldClrType: typeof(string), oldType: "nvarchar(max)"); } |
Optando por manter nvarchar(max)
Se você precisa manter compatibilidade com versões anteriores do SQL Server, pode forçar o tipo de coluna:
|
1 2 3 4 5 6 7 8 9 10 |
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Pedido>() .ComplexProperty(p => p.Detalhes, b => { b.ToJson(); b.HasColumnType("nvarchar(max)"); <em>// Força tipo texto</em> }); } |
Ou configure um nível de compatibilidade inferior a 170:
|
1 2 3 |
optionsBuilder.UseSqlServer(connectionString, o => o.UseCompatibilityLevel(160)); <em>// SQL Server 2022</em> |
Tipos complexos vs. Owned Entities
O EF Core 10 recomenda usar tipos complexos em vez de owned entities para mapeamento JSON. A diferença é semântica:
| Aspecto | Tipos Complexos | Owned Entities |
|---|---|---|
| Semântica | Valor (cópia) | Referência |
| ExecuteUpdate | Suportado | Não suportado |
| Atribuição | Copia propriedades | Erro se reutilizar |
| Comparação LINQ | Por conteúdo | Por identidade |
Exemplo com tipos complexos (recomendado)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public class Cliente { public int Id { get; set; } public string Nome { get; set; } = string.Empty; public Endereco EnderecoEntrega { get; set; } = new(); public Endereco? EnderecoCobranca { get; set; } } public class Endereco { public string Rua { get; set; } = string.Empty; public string Cidade { get; set; } = string.Empty; public string CEP { get; set; } = string.Empty; } <em>// Configuração</em> modelBuilder.Entity<Cliente>(entity => { entity.ComplexProperty(c => c.EnderecoEntrega, e => e.ToJson()); entity.ComplexProperty(c => c.EnderecoCobranca, e => e.ToJson()); }); |
Comportamento de atribuição
|
1 2 3 4 5 6 7 8 9 10 |
var cliente = await context.Clientes.FindAsync(1); <em>// Com tipos complexos: copia as propriedades (funciona)</em> cliente.EnderecoCobranca = cliente.EnderecoEntrega; await context.SaveChangesAsync(); <em>// OK</em> <em>// Ambos os endereços são independentes após a cópia</em> cliente.EnderecoEntrega.Cidade = "São Paulo"; <em>// EnderecoCobranca.Cidade permanece inalterado</em> |
Suporte a structs
O EF Core 10 também suporta struct como tipo complexo, útil para tipos de valor pequenos:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public struct Coordenada { public double Latitude { get; set; } public double Longitude { get; set; } } public class Loja { public int Id { get; set; } public string Nome { get; set; } = string.Empty; public Coordenada Localizacao { get; set; } } <em>// Configuração</em> modelBuilder.Entity<Loja>() .ComplexProperty(l => l.Localizacao, c => c.ToJson()); |
Coleções primitivas como JSON
Arrays e listas de tipos primitivos são automaticamente mapeados como JSON:
|
1 2 3 4 5 6 7 8 |
public class Produto { public int Id { get; set; } public string Nome { get; set; } = string.Empty; public string[] Tags { get; set; } = []; public List<decimal> HistoricoPrecos { get; set; } = []; } |
Sem configuração adicional, o EF Core 10 gera:
|
1 2 3 4 5 6 7 8 |
CREATE TABLE [Produtos] ( [Id] int NOT NULL IDENTITY, [Nome] nvarchar(max) NOT NULL, [Tags] json NOT NULL, [HistoricoPrecos] json NOT NULL, CONSTRAINT [PK_Produtos] PRIMARY KEY ([Id]) ); |
Consultando coleções
|
1 2 3 4 5 6 7 8 9 10 |
<em>// Produtos com tag específica</em> var produtos = await context.Produtos .Where(p => p.Tags.Contains("promoção")) .ToListAsync(); <em>// Produtos com mais de 5 variações de preço</em> var produtosVolateis = await context.Produtos .Where(p => p.HistoricoPrecos.Count > 5) .ToListAsync(); |
Limitações conhecidas
Issue #37275: Migração de ToJson() existente
Há um bug conhecido (corrigido na versão 10.0.2) onde colunas criadas com ToJson() no EF Core 9 não são automaticamente convertidas para json na migração:
- Projetos novos: Funcionam corretamente
- Coleções primitivas: Migram corretamente
- ToJson() do EF Core 9: Podem requerer migração manual
Workaround: Adicione manualmente a alteração de coluna na migração:
|
1 2 3 4 5 6 7 8 |
migrationBuilder.AlterColumn<string>( name: "Detalhes", table: "Pedidos", type: "json", nullable: false, oldClrType: typeof(string), oldType: "nvarchar(max)"); |
Compatibilidade de providers
O tipo json nativo é específico do SQL Server 2025 e Azure SQL. Para outros providers:
- PostgreSQL (Npgsql): Use
jsonbcomHasColumnType("jsonb") - SQLite: Armazena como texto
- MySQL: Suporte varia por versão
Comparativo: nvarchar(max) vs json nativo
| Aspecto | nvarchar(max) | json nativo |
|---|---|---|
| Validação | Nenhuma | JSON válido obrigatório |
| Armazenamento | Texto UTF-16 | Binário otimizado |
| Parsing | A cada query | Pré-processado |
| Funções SQL | JSON_VALUE(), JSON_QUERY() | + RETURNING, modify() |
| ExecuteUpdate | Reescreve documento | Atualização parcial |
| Indexação | Colunas computadas | Suporte nativo futuro |
Exemplo completo: API de pedidos
|
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 |
using Microsoft.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(args); builder.Services.AddDbContext<LojaDbContext>(options => options.UseAzureSql(builder.Configuration.GetConnectionString("Loja"))); var app = builder.Build(); <em>// Criar pedido com JSON</em> app.MapPost("/pedidos", async (LojaDbContext db, CriarPedidoRequest request) => { var pedido = new Pedido { Numero = Guid.NewGuid().ToString("N")[..8].ToUpper(), DataCriacao = DateTime.UtcNow, Tags = request.Tags, Detalhes = new DetalhesPedido { Observacao = request.Observacao, Desconto = request.Desconto, Prioridade = request.Prioridade }, Itens = request.Itens.Select(i => new ItemPedido { Produto = i.Produto, Quantidade = i.Quantidade, PrecoUnitario = i.PrecoUnitario }).ToList() }; db.Pedidos.Add(pedido); await db.SaveChangesAsync(); return Results.Created($"/pedidos/{pedido.Id}", pedido); }); <em>// Buscar pedidos urgentes</em> app.MapGet("/pedidos/urgentes", async (LojaDbContext db) => { return await db.Pedidos .Where(p => p.Detalhes.Prioridade > 5) .OrderByDescending(p => p.Detalhes.Prioridade) .ToListAsync(); }); <em>// Aumentar prioridade em lote</em> app.MapPost("/pedidos/aumentar-prioridade", async (LojaDbContext db) => { var atualizados = await db.Pedidos .Where(p => p.DataCriacao.Date == DateTime.Today) .ExecuteUpdateAsync(s => s.SetProperty(p => p.Detalhes.Prioridade, p => p.Detalhes.Prioridade + 1)); return Results.Ok(new { PedidosAtualizados = atualizados }); }); app.Run(); <em>// Records para requests</em> public record CriarPedidoRequest( string[] Tags, string? Observacao, decimal Desconto, int Prioridade, List<ItemPedidoRequest> Itens); public record ItemPedidoRequest( string Produto, int Quantidade, decimal PrecoUnitario); |
Conclusão
O suporte nativo ao tipo json no EF Core 10 representa uma evolução significativa para aplicações que trabalham com dados semi-estruturados. A combinação de validação automática, armazenamento eficiente e queries otimizadas elimina compromissos que desenvolvedores precisavam aceitar ao usar JSON no SQL Server.
Para projetos novos com SQL Server 2025 ou Azure SQL, não há razão para não usar o tipo nativo. Para projetos existentes, a migração é automática na maioria dos casos — basta atualizar o EF Core e configurar o nível de compatibilidade.
FAQ: Perguntas Frequentes
1. Preciso do SQL Server 2025 para usar JSON no EF Core 10?
Não. O EF Core 10 continua suportando JSON em versões anteriores do SQL Server usando nvarchar(max). O tipo json nativo é um recurso adicional disponível apenas no SQL Server 2025 e Azure SQL Database.
2. Meus dados JSON existentes serão afetados na migração?
Os dados são preservados. A migração altera apenas o tipo da coluna de nvarchar(max) para json. O conteúdo permanece intacto, desde que seja JSON válido. Se houver JSON inválido, a migração falhará.
3. Posso usar o tipo json com PostgreSQL?
O PostgreSQL tem seu próprio tipo jsonb, que é mais maduro e performático. O provider Npgsql suporta isso via HasColumnType("jsonb"). O comportamento é similar, mas a implementação é específica do PostgreSQL.
4. ExecuteUpdate com JSON funciona em versões anteriores do SQL Server?
Sim, mas com sintaxe SQL diferente. O EF Core 10 detecta a versão do SQL Server e gera o SQL apropriado. Em versões anteriores, a atualização reescreve o documento JSON inteiro em vez de usar JSON_MODIFY().
5. Qual a diferença entre ComplexProperty e OwnsOne para JSON?
ComplexProperty é a abordagem recomendada no EF Core 10. Usa semântica de valor (copia propriedades na atribuição), suporta ExecuteUpdate, e tem melhor performance. OwnsOne ainda funciona para compatibilidade, mas é considerado legado para cenários JSON.