Pular para o conteúdo principal

Entidades Dinâmicas no EFCore - Método 2

A resposta direta para o uso de DynamicObject no Entity Framework Core é: o EF Core não suporta o uso de DynamicObject ou do tipo dynamic diretamente como entidade ou propriedade de mapeamento.

Isso ocorre porque o C# não consegue gerar Árvores de Expressão (Expression Trees) para tipos dinâmicos, o que impede o LINQ de traduzir suas propriedades para consultas SQL. Além disso, o modelo de metadados do EF Core precisa ser rigidamente tipado em tempo de compilação/inicialização.

No entanto, é totalmente possível atingir exatamente a experiência de ler e escrever propriedades dinâmicas (produto.Preco = 10) no EF Core combinando DynamicObject com a funcionalidade de colunas JSON (nativa a partir do EF Core 7+).

Abaixo está o tutorial completo, do início ao fim, mostrando a arquitetura correta para isso.

Como Funciona a Solução

  1. A entidade armazenará os dados flexíveis dentro de uma string JSON no banco de dados.

  2. Criaremos uma classe derivada de DynamicObject que intercepta a leitura (TryGetMember) e a escrita (TrySetMember) de propriedades dinâmicas e sincroniza com o JSON.

  3. Usaremos Value Converters do EF Core para converter automaticamente o JSON da tabela do banco de dados na nossa classe dinâmica ao ler/gravar.

Passo 1: Criar a classe DynamicBag (herdada de DynamicObject)

Esta classe gerenciará as propriedades flexíveis como um dicionário de chave-valor na memória e lidará com a serialização/deserialização JSON.

C#
using System.Dynamic;
using System.Text.Json;

public class DynamicBag : DynamicObject
{
    // Dicionário interno para armazenar as propriedades dinâmicas
    public Dictionary<string, object?> Properties { get; set; } = new();

    // Intercepta a leitura de propriedades (ex: var x = entidade.Bag.Preco)
    public override bool TryGetMember(GetMemberBinder binder, out object? result)
    {
        return Properties.TryGetValue(binder.Name, out result);
    }

    // Intercepta a atribuição de propriedades (ex: entidade.Bag.Preco = 99.90)
    public override bool TrySetMember(SetMemberBinder binder, object? value)
    {
        Properties[binder.Name] = value;
        return true;
    }

    // Converte a estrutura para JSON para ser salva no banco
    public string ToJson()
    {
        return JsonSerializer.Serialize(Properties);
    }

    // Cria uma instância de DynamicBag a partir de uma string JSON vinda do banco
    public static DynamicBag FromJson(string json)
    {
        var bag = new DynamicBag();
        if (string.IsNullOrWhiteSpace(json)) return bag;

        var rawDict = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(json);
        if (rawDict != null)
        {
            foreach (var kvp in rawDict)
            {
                bag.Properties[kvp.Key] = ConvertJsonElement(kvp.Value);
            }
        }
        return bag;
    }

    // Auxiliar para converter tipos nativos do System.Text.Json em tipos C# primitivos
    private static object? ConvertJsonElement(JsonElement element) => element.ValueKind switch
    {
        JsonValueKind.String => element.GetString(),
        JsonValueKind.Number => element.TryGetInt64(out long l) ? l : element.GetDouble(),
        JsonValueKind.True => true,
        JsonValueKind.False => false,
        JsonValueKind.Null => null,
        _ => element.GetRawText()
    };
}

Passo 2: Criar a Entidade do EF Core

Agora crie sua entidade tipada. Note que a propriedade dinâmica Atributos é do tipo dynamic, mas por baixo é instanciada como nosso DynamicBag.

C#
public class Produto
{
    public int Id { get; set; }
    public string Nome { get; set; } = string.Empty;

    // Propriedade dynamic exposta para a aplicação
    public dynamic Atributos { get; set; } = new DynamicBag();
}

Passo 3: Configurar o DbContext com Value Converter

No seu DbContext, você precisa instruir o EF Core a converter a classe DynamicBag em uma coluna de texto/JSON no banco de dados usando HasConversion.

C#
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;

public class AppDbContext : DbContext
{
    public DbSet<Produto> Produtos => Set<Produto>();

    protected override OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        // Exemplo usando SQLite (funciona perfeitamente em SQL Server, PostgreSQL, etc.)
        optionsBuilder.UseSqlite("Data Source=app.db");
    }

    protected override OnModelCreating(ModelBuilder modelBuilder)
    {
        // Criamos o conversor entre DynamicBag e string (JSON)
        var converter = new ValueConverter<dynamic, string>(
            v => ((DynamicBag)v).ToJson(),
            v => DynamicBag.FromJson(v)
        );

        modelBuilder.Entity<Produto>(entity =>
        {
            entity.HasKey(p => p.Id);
            entity.Property(p => p.Nome).IsRequired();

            // Mapeia a propriedade 'Atributos' como coluna texto/json no banco
            entity.Property(p => p.Atributos)
                  .HasConversion(converter)
                  .HasColumnType("nvarchar(max)"); // Em PostgreSQL use "jsonb", em SQLite use "TEXT"
        });
    }
}

Passo 4: Exemplo Completo de Uso (Criação, Leitura e Atualização)

Veja como manipular os dados dinâmica e fortemente tipados na aplicação:

C#
using System;
using System.Linq;

class Program
{
    static void Main()
    {
        // 1. Inicializar Banco de Dados
        using (var db = new AppDbContext())
        {
            db.Database.EnsureDeleted();
            db.Database.EnsureCreated();

            // 2. INSERIR DADOS DINÂMICOS
            var produto1 = new Produto
            {
                Nome = "Smartphone Top"
            };

            // Adicionando propriedades livremente em tempo de execução
            produto1.Atributos.Cor = "Preto";
            produto1.Atributos.ArmazenamentoGB = 256;
            produto1.Atributos.Possui5G = true;

            db.Produtos.Add(produto1);
            db.SaveChanges();
            Console.WriteLine("Produto salvo com sucesso!");
        }

        // 3. LER E RECUPERAR DADOS DINÂMICOS
        using (var db = new AppDbContext())
        {
            var produtoBD = db.Produtos.First();

            Console.WriteLine($"\nId: {produtoBD.Id}");
            Console.WriteLine($"Nome: {produtoBD.Nome}");
            
            // Acessando diretamente como membros estáticos graças ao DynamicObject
            Console.WriteLine($"Cor: {produtoBD.Atributos.Cor}");
            Console.WriteLine($"Memória: {produtoBD.Atributos.ArmazenamentoGB} GB");
            Console.WriteLine($"Suporta 5G: {produtoBD.Atributos.Possui5G}");

            // 4. ATUALIZAR PROPRIEDADE DINÂMICA
            produtoBD.Atributos.Cor = "Azul Titanium";
            produtoBD.Atributos.NovoCampoGenerico = "Adicionado depois";
            
            db.SaveChanges();
        }
    }
}

Cuidados Importantes e Limitações

  1. Filtros em Consultas LINQ:

    Você não pode fazer filtros LINQ diretos nas propriedades do DynamicObject como db.Produtos.Where(p => p.Atributos.Cor == "Preto"). O C# lançará exceção de Expression Tree.

    • Alternativa no EF Core 8/9+: Se precisar fazer queries dentro do JSON, utilize a API nativa do EF Core EF.Functions.JsonContains ou consulte as colunas JSON mapeadas diretamente como tipos de propriedade complexa/JSON.

  2. Detecção de Mudanças (Change Tracking):

    Ao alterar uma propriedade dentro de produto.Atributos (ex: produto.Atributos.Cor = "Vermelho"), como Atributos é um objeto de referência cujo ponteiro não mudou, o EF Core pode não notar automaticamente que ele mudou.

    Para garantir a persistência em atualizações, marque a propriedade como modificada se necessário:

    C#
    db.Entry(produto).Property(p => p.Atributos).IsModified = true;
    

No video Entity Framework Core Part 10 - Dynamic Mapping, você pode acompanhar discussões e cenários alternativos sobre como lidar com comportamentos e mapeamentos dinâmicos diretamente dentro do pipeline do Entity Framework Core.

Comentários

Postagens mais visitadas deste blog

UML e comunicação entre Componentes

(conteúdo gerado com I.A.) Antes de abordarmos o conceito de Componentes, vale a pena analisar como um programador iniciante costuma estruturar uma aplicação. Geralmente, a escolha é construir um monólito tradicional, agrupando todas as classes de domínio (como Categoria, Produto, Compra, Venda, Pagamento, etc.) em uma única pasta de Entidades. Da mesma forma, criam-se pacotes globais para Repositórios, Serviços e Controllers. O grande problema dessa abordagem é que ela gera um forte acoplamento entre as classes e as camadas do sistema. À medida que a aplicação cresce, isso desencadeia uma série de dificuldades: Manutenção e modelagem complexas: As responsabilidades se misturam. Rapidamente, o código passa a ter diversos trechos onde as classes assumem funções que não deveriam ter, tornando a evolução do sistema confusa. Escalabilidade ineficiente: Para escalar o backend, é necessário replicar a aplicação inteira e todo o banco de dados. Além de consumir mais recursos, isso traz o ri...

Quando escolher Flask ou Quart no Python

Atualmente , considerando o suporte assíncrono já presente no Flask, a escolha entre Flask e Quart para uma API Server depende de vários fatores técnicos. Vejamos alguns critérios: Fonte:  https://pt.stackoverflow.com/questions/51268/qual-a-diferen%C3%A7a-entre-comunica%C3%A7%C3%A3o-ass%C3%ADncrona-e-s%C3%ADncrona Suporte Assíncrono : Flask : O Flask oferece suporte assíncrono por meio de extensões de terceiros, como  flask[async] . No entanto, seu desempenho assíncrono é inferior aos frameworks nativamente assíncronos. Quart : O Quart é uma reimplantação do Flask baseada no padrão ASGI (Asynchronous Server Gateway Interface). Ele utiliza E/S assíncrona em todo o código e é mais adequado para bases de código predominantemente assíncronas. Desempenho : Flask : Como aplicação WSGI, o Flask usa um único worker para cada ciclo de solicitação/resposta. Quando uma solicitação chega a uma visualização assíncrona, o Flask inicia um loop de eventos em uma thread, executa a função da vi...

Metodo para criar relacionamento entre tabelas sem dar erro cíclico EFCore

Quando estamos modelando o banco de dados no Entity Framework Core (ou em qualquer outra ferramenta) é muito comum criarmos o erro cíclico. Este erro ocorre devido ao banco de dados não conseguir determinar onde o relacionamento termina, causando um erro de loop. Vejamos uma situação real onde foi gerado esse problema: Um Professor leciona em várias Turmas, Um Professor leciona uma ou mais Disciplinas, Um Professor leciona determinadas Disciplinas em determinadas Turmas, Uma Turma tem muitos Professores, Em todas as Turmas tem 9 Disciplinas. Em todas as turmas podem ter até 9 Professores. Só por essa descrição já é possível perceber que os relacionamentos podem ser complexos de entender. E tem-se também o problema da navegação em via dupla: Professor-Turmas, Turma-Professores. Então, como modelar essas tabelas sem que o banco entre em loop? Para resolver esse problema no Entity Framework Core e evitar o erro de "Multiple Cascade Paths" (caminhos múltiplos de cascata) comum n...