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):
// EF Core 7/8/9 - JSON armazenado como nvarchar(max)
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Pedido>()
.OwnsOne(p => p.Detalhes, b => b.ToJson());
}
SQL gerado (EF Core 9 e anteriores):
CREATE TABLE [Pedidos] (
[Id] int NOT NULL IDENTITY,
[Numero] nvarchar(max) NOT NULL,
[Detalhes] nvarchar(max) NOT NULL, -- JSON como texto
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:
Como habilitar
Opção 1: Azure SQL Database
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
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
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
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 =>
{
// Mapeia DetalhesPedido como JSON
entity.ComplexProperty(p => p.Detalhes, b => b.ToJson());
// Mapeia coleção de itens como JSON
entity.OwnsMany(p => p.Itens, b => b.ToJson());
});
}
}
SQL gerado na migração
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
// Buscar pedidos com prioridade alta
var pedidosUrgentes = await context.Pedidos
.Where(p => p.Detalhes.Prioridade > 5)
.ToListAsync();
SQL gerado:
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
// Ordenar por desconto (maior primeiro)
var pedidosOrdenados = await context.Pedidos
.OrderByDescending(p => p.Detalhes.Desconto)
.Take(10)
.ToListAsync();
Filtrar por texto dentro do JSON
// Buscar pedidos com observação específica
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
// Aumentar prioridade de todos os pedidos do dia
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):
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
// Adicionar prefixo à observação
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
// Remover desconto de pedidos antigos
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:
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:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Pedido>()
.ComplexProperty(p => p.Detalhes, b =>
{
b.ToJson();
b.HasColumnType("nvarchar(max)"); // Força tipo texto
});
}
Ou configure um nível de compatibilidade inferior a 170:
optionsBuilder.UseSqlServer(connectionString,
o => o.UseCompatibilityLevel(160)); // SQL Server 2022
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)
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;
}
// Configuração
modelBuilder.Entity<Cliente>(entity =>
{
entity.ComplexProperty(c => c.EnderecoEntrega, e => e.ToJson());
entity.ComplexProperty(c => c.EnderecoCobranca, e => e.ToJson());
});
Comportamento de atribuição
var cliente = await context.Clientes.FindAsync(1);
// Com tipos complexos: copia as propriedades (funciona)
cliente.EnderecoCobranca = cliente.EnderecoEntrega;
await context.SaveChangesAsync(); // OK
// Ambos os endereços são independentes após a cópia
cliente.EnderecoEntrega.Cidade = "São Paulo";
// EnderecoCobranca.Cidade permanece inalterado
Suporte a structs
O EF Core 10 também suporta struct como tipo complexo, útil para tipos de valor pequenos:
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; }
}
// Configuração
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:
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:
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
// Produtos com tag específica
var produtos = await context.Produtos
.Where(p => p.Tags.Contains("promoção"))
.ToListAsync();
// Produtos com mais de 5 variações de preço
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:
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
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<LojaDbContext>(options =>
options.UseAzureSql(builder.Configuration.GetConnectionString("Loja")));
var app = builder.Build();
// Criar pedido com JSON
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);
});
// Buscar pedidos urgentes
app.MapGet("/pedidos/urgentes", async (LojaDbContext db) =>
{
return await db.Pedidos
.Where(p => p.Detalhes.Prioridade > 5)
.OrderByDescending(p => p.Detalhes.Prioridade)
.ToListAsync();
});
// Aumentar prioridade em lote
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();
// Records para requests
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.