Conteúdo

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 aprimoradasJSON_VALUE() RETURNINGmodify() e outras

Requisitos

Para usar o tipo json nativo, você precisa de:

  1. SQL Server 2025 ou Azure SQL Database
  2. 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 TagsDetalhes 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:

AspectoTipos ComplexosOwned Entities
SemânticaValor (cópia)Referência
ExecuteUpdateSuportadoNão suportado
AtribuiçãoCopia propriedadesErro se reutilizar
Comparação LINQPor conteúdoPor 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 jsonb com HasColumnType("jsonb")
  • SQLite: Armazena como texto
  • MySQL: Suporte varia por versão

Comparativo: nvarchar(max) vs json nativo

Aspectonvarchar(max)json nativo
ValidaçãoNenhumaJSON válido obrigatório
ArmazenamentoTexto UTF-16Binário otimizado
ParsingA cada queryPré-processado
Funções SQLJSON_VALUE()JSON_QUERY()RETURNINGmodify()
ExecuteUpdateReescreve documentoAtualização parcial
IndexaçãoColunas computadasSuporte 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.

Compartilhe:

Tiago Tartari

Tiago Tartari

Eu ajudo e capacito pessoas e organizações a transformar problemas complexos em soluções práticas usando a tecnologia para atingir resultados extraordinários.

Qual é o desafio
que você tem hoje?