15
respostas

[Dúvida] Não estou conseguindo persistir a Nota no meu Banco

Aparentemente fazendo o debugging ele a lógica está correta, mas apesar dele voltar um 201 e retornar o dado no método Post, quando eu executo o método Get ele volta nulo, porque a nota não foi persistida.

Post:

Insira aqui a descrição dessa imagem para ajudar na acessibilidade

Get:

Insira aqui a descrição dessa imagem para ajudar na acessibilidade

15 respostas

Avaliação:

using ScreenSound.Modelos;

namespace ScreenSound.Shared.Models.Models;

public class AvaliacaoArtista
{
    public virtual Artista? Artista { get; set; }
    public int ArtistaId { get; set; }
    public int PessoaId { get; set; }
    public int Nota { get; set; }

}

Artista:

public string Nome { get; set; }
public string FotoPerfil { get; set; }
public string Bio { get; set; }
public int Id { get; set; }
public virtual ICollection<Musica> Musicas { get; set; } = new List<Musica>();
public virtual ICollection<AvaliacaoArtista> Avaliacoes { get; set; } = new List<AvaliacaoArtista>();
...

public void AdicionarNota(int pessoaId, int nota)
{
    nota = Math.Clamp(nota, 1, 5); // Garante que a nota esteja entre 1 e 5
    Avaliacoes.Add(new AvaliacaoArtista() { ArtistaId = this.Id, PessoaId = pessoaId, Nota = nota });
}

Request:

namespace ScreenSound.API.Requests;

public record AvaliacaoArtistaRequest(int ArtistaId, int Nota);

Response:

using System.ComponentModel.DataAnnotations;

namespace ScreenSound.API.Responses;

public record AvaliacaoArtistaResponse([Required] int ArtistaId, [Required] int Nota);

Program:

builder.Services.AddTransient<DAL<PessoaComAcesso>>();

Endpoint:

// Rota que permite avaliar um artista
groupBuilder.MapPost("avaliacao", (HttpContext context, [FromBody] AvaliacaoArtistaRequest request, [FromServices] DAL<Artista> dalArtista, [FromServices] DAL<PessoaComAcesso> dalPessoaComAcesso) =>
{
    var artista = dalArtista.FindBy(a => a.Id == request.ArtistaId);

    if (artista is null) return Results.NotFound();

    // O ?? é um operador de coalescência nula, que retorna o valor da esquerda se não for nulo, senão o valor da direita
    var email = context.User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value
        ?? throw new InvalidOperationException("Usuário não autenticado");

    // Se o email não for encontrado, lança uma exceção de InvalidOperationException com a mensagem "Pessoa não encontrada"
    var pessoa = dalPessoaComAcesso.FindBy(p => p.Email == email)
        ?? throw new InvalidOperationException("Pessoa não encontrada");

    // Verifica se a pessoa já avaliou o artista, passando o id do artista e o id da pessoa
    var avaliacao = artista.Avaliacoes.FirstOrDefault(a => a.ArtistaId == artista.Id && a.PessoaId == pessoa.Id);

    // Se a avaliação não existir, adiciona a nota
    if (avaliacao is null)
    {
        // Adiciona a nota
        artista.AdicionarNota(pessoa.Id, request.Nota);
    }
    else
    {
        // Se a avaliação existir, atualiza a nota
        dalArtista.Atualizar(artista);
    }

    return Results.Created();
});

// Rota que busca a nota do artista
groupBuilder.MapGet("{id}/avaliacao", (int id, HttpContext context, [FromServices] DAL<Artista> dalArtista, [FromServices] DAL<PessoaComAcesso> dalPessoa) =>
{
    // Valida se o artista existe
    var artista = dalArtista.FindBy(a => a.Id == id);
    if (artista is null) return Results.NotFound();

    // Verifica se a pessoa está autenticada
    var email = context.User.Claims.FirstOrDefault(c => c.Type.Equals(ClaimTypes.Email))?.Value
        ?? throw new InvalidOperationException("Não foi encontrado o email da pessoa logada");

    // Verifica se a pessoa existe
    var pessoa = dalPessoa.FindBy(p => p.Email!.Equals(email))
        ?? throw new InvalidOperationException("Não foi encontrado o email da pessoa logada");

    // Busca a avaliação do artista feita pela pessoa, passando o id do artista e o id da pessoa
    var avaliacao = artista.Avaliacoes.FirstOrDefault(a => a.ArtistaId == id && a.PessoaId == pessoa.Id);

    // Se a avaliação não existir, retorna 0
    if (avaliacao is null)
    {
        // Retorna a nota 0
        return Results.Ok(new AvaliacaoArtistaResponse(id, 0));
    }
    else
    {
        // Se a avaliação existir, retorna a nota
        return Results.Ok(new AvaliacaoArtistaResponse(id, avaliacao.Nota));
    }

});

Consegui fazer ele persistir o dado no banco, tinha colocado o método atualizar dento do if, mas na verdade ele é fora, pois é ele quem vai persistir meu dado no banco.

Trecho do código do endpoint:

groupBuilder.MapPost...

     // Se a avaliação não existir, adiciona a nota
    if (avaliacao is null)
    {
        // Adiciona a nota
        artista.AdicionarNota(pessoa.Id, request.Nota);
    }
    else
    {
        // Se a avaliação existir, atualiza a nota
        avaliacao.Nota = request.Nota;                
    }

    // Se a avaliação existir, atualiza a nota
    dalArtista.Atualizar(artista);

    return Results.Created();
});

Agora eu estou com um erro que ele não consegue ir lá no meu DB e voltar o dado pra mim. E por conta disso ele está dando erro, porque ele acha que não tem nada e tenta adicionar novamente um dado que já existe.

Verificando o código, tando no Post quando no Get, o trecho que está com problema é onde ele faz a verificação da avaliação, porque está voltando null.

// Verifica se a pessoa já avaliou o artista, passando o id do artista e o id da pessoa
var avaliacao = artista.Avaliacoes.FirstOrDefault(a => a.ArtistaId == artista.Id && a.PessoaId == pessoa.Id);

Essa abordagem seria meio que uma solução para continuar o curso, mas não é bem o foco. Porque o objetivo é popular a coleção de Avaliações em Artistas.

Solução temporária:

Insira aqui a descrição dessa imagem para ajudar na acessibilidade

 var avaliacao = artista.Avaliacoes.FirstOrDefault(a => a.ArtistaId == id && a.PessoaId == pessoa.Id);

 var dalAvaliacao = dalAvaliacaoArtista.FindBy(av => av.ArtistaId == id && av.PessoaId == pessoa.Id);

 avaliacao = dalAvaliacao;

De fato a "solução" que eu encontrei ajuda a gravar no banco de dados. Mas essa gambiarra não me dá muitos benefícios, porque eu preciso que a lista em Artista seja populada e eu não estou conseguindo um retorno satisfatório com essa gambiarra, para que a classificação me retorne a média das notas.

Segue o meu github, para ajudar a visualizar o código.

https://github.com/IgorTudisco/Entity-Framework-Core/tree/main/ScreenSound.API

Oi, Igor! Tudo bem?

O problema pode estar na forma como você está carregando os dados do banco. O EF Core, por padrão, não carrega relacionamentos automaticamente. Você precisa garantir que a lista de avaliações está sendo carregada corretamente quando você busca o artista.

Ajuste a consulta para incluir as avaliações usando Include:

var artista = dalArtista
    .FindBy(a => a.Id == id)
    .Include(a => a.Avaliacoes);

Se você estiver usando um repositório genérico, talvez seja necessário modificar o método FindBy para permitir Includings. Agora, se o problema persistir, verifique se o banco realmente tem os dados persistidos. Rode um SELECT diretamente no banco para conferir.

Espero ter ajudado e bons estudos!

Caso este post tenha lhe ajudado, por favor, marcar como solucionado ✓.

Eu não sei se entendi direito, ma ficaria assim?


using Microsoft.EntityFrameworkCore;
using System.Linq.Expressions;

namespace ScreenSound.Data;

public class DAL<T> where T : class
{

    private readonly ScreenSoundContext _context;
    private readonly DbSet<T> _dbSet;

    public DAL(ScreenSoundContext context)
    {
        _context = context;
        _dbSet = _context.Set<T>();
    }

    public IEnumerable<T> QueryIncludes(params Expression<Func<T, object?>>[] includes)
    {
        IQueryable<T> query = _dbSet;

        // Aplica os includes dinâmicos
        foreach (var include in includes)
        {
            query = query.Include(include);
        }

        return query.ToList();
    }

    public IEnumerable<T> Listar()
    {
        return _dbSet.ToList();
    }

    public IEnumerable<T> Listar(params Expression<Func<T, object?>>[] includes)
    {
        return QueryIncludes(includes);
    }

    public void Adicionar(T objeto)
    {
        _dbSet.Add(objeto);
        _context.SaveChanges();
    }

    public void Atualizar(T objeto)
    {
        _dbSet.Update(objeto);
        _context.SaveChanges();
    }

    public void Excluir(T objeto)
    {
        _context.Set<T>().Remove(objeto);
        _context.SaveChanges();
    }

    public T? FindBy(Func<T, bool> condition)
    {
        var obj = _dbSet.FirstOrDefault(condition);
        return obj;
    }

    public IEnumerable<T?> FindBy(Func<T, bool> condicao, params Expression<Func<T, object?>>[] includes)
    {
        var list = QueryIncludes(includes);
        return list.Where(condicao).ToList();
    }
}

Oi, Igor!

Sobre sua última dúvida, o motivo pelo qual a lista Avaliacoes está vindo vazia é porque o método FindBy que você usou não aplica os includes na versão sem os parâmetros de includes.

Ou seja, quando você chama:


var artista = dalArtista.FindBy(a => a.Id == id);

você não está incluindo a lista de avaliações, o que faz com que ela venha vazia mesmo que os dados estejam no banco.

Para resolver isso, use a versão sobrecarregada do FindBy com includes, dessa forma:


var artista = dalArtista
    .FindBy(a => a.Id == id, a => a.Avaliacoes)
    .FirstOrDefault();

Essa chamada vai garantir que a coleção Avaliacoes seja populada junto com o artista.

Importante: veja que seu método FindBy com includes retorna uma lista (IEnumerable<T>), então é necessário usar .FirstOrDefault() após a chamada para obter um único resultado.

Dica extra: se em algum momento você quiser incluir também o Artista dentro de cada avaliação, adicione um include aninhado assim:


a => a.Avaliacoes, a => a.Avaliacoes.Select(av => av.Artista)

Mas para o seu caso atual, somente a => a.Avaliacoes já resolve o problema.

Fico à disposição.

Entendi, eu também poderia usar assim pelo Linq?

var musicaAtualizar = dalMusica.QueryIncludes(a => a.Artista).FirstOrDefault(m => m.Id == musicaRequestEdit.Id);

Se sim, qual a diferença? Seria por conta do DAL?

Oi, Igor!

Sobre sua última dúvida, perfeito, você pode sim utilizar o método QueryIncludes dessa forma com LINQ:


var musicaAtualizar = dalMusica
    .QueryIncludes(a => a.Artista)
    .FirstOrDefault(m => m.Id == musicaRequestEdit.Id);

A diferença aqui está no controle da consulta. Quando você usa QueryIncludes, está trabalhando diretamente com um IQueryable, ou seja, pode encadear filtros como FirstOrDefault, Where, OrderBy, etc., antes de executar a consulta no banco.

Já o método FindBy que você criou retorna um IEnumerable, o que significa que ele executa o ToList() internamente, trazendo todos os registros que atendem aos includes, e só depois aplica o filtro no método Where ou FirstOrDefault, mas em memória.

Portanto, usando QueryIncludes, o filtro ocorre diretamente no banco de dados, otimizando a consulta. Veja a diferença prática:

  • QueryIncludes: Filtra no banco (mais performático).
  • FindBy: Traz dados para a memória e depois filtra (menos performático se tiver muitos registros).

Se quiser deixar o DAL ainda mais robusto, uma sugestão é criar uma versão do FindBy que retorne um IQueryable ao invés de IEnumerable, assim você mantém a flexibilidade de filtrar antes de executar a query.

Fico à disposição.

Ficaria mais ou menos assim?

using Microsoft.EntityFrameworkCore;
using System.Linq.Expressions;

namespace ScreenSound.Data;

public class DAL<T> where T : class
{

    private readonly ScreenSoundContext _context;
    private readonly DbSet<T> _dbSet;

    public DAL(ScreenSoundContext context)
    {
        _context = context;
        _dbSet = _context.Set<T>();
    }

    public IEnumerable<T> QueryIncludes(params Expression<Func<T, object?>>[] includes)
    {
        IQueryable<T> query = _dbSet;

        // Aplica os includes dinâmicos
        foreach (var include in includes)
        {
            query = query.Include(include);
        }

        return query.ToList();
    }

    public IEnumerable<T> Listar()
    {
        return _dbSet.ToList();
    }

    public IEnumerable<T> Listar(params Expression<Func<T, object?>>[] includes)
    {
        return QueryIncludes(includes);
    }

    public void Adicionar(T objeto)
    {
        _dbSet.Add(objeto);
        _context.SaveChanges();
    }

    public void Atualizar(T objeto)
    {
        _dbSet.Update(objeto);
        _context.SaveChanges();
    }

    public void Excluir(T objeto)
    {
        _context.Set<T>().Remove(objeto);
        _context.SaveChanges();
    }

    public T? FindBy(Func<T, bool> condition)
    {
        var obj = _dbSet.FirstOrDefault(condition);
        return obj;
    }

    public IEnumerable<T?> FindBy(Func<T, bool> condicao, params Expression<Func<T, object?>>[] includes)
    {
        var list = QueryIncludes(includes);
        return list.Where(condicao).ToList();
    }
    
    // Seguindo a sua sugestão

    public IQueryable<T> FindBy()
    {
       return _dbSet.AsQueryable();
    }
}

Oi, Igor!

Essa forma que você implementou, o método:

public IQueryable<T> FindBy()
{
    return _dbSet.AsQueryable();
}

Não é exatamente certo para seu objetivo, pois ele não permite nem aplicar filtros, nem carregar os relacionamentos com Include.

O ideal é ajustar assim, permitindo filtros e includes dinâmicos:

public IQueryable<T> FindBy(Expression<Func<T, bool>>? filtro = null, params Expression<Func<T, object?>>[] includes)
{
    IQueryable<T> query = _dbSet;

    foreach (var include in includes)
    {
        query = query.Include(include);
    }

    if (filtro is not null)
    {
        query = query.Where(filtro);
    }

    return query;
}

Assim você mantém a flexibilidade de filtrar e incluir os relacionamentos de forma performática e correta.

Fico à disposição.

Poderia se possível, me explicar melhor essa função? Não entendi direito como construir ela.

Oi, Igor!

Vamos entender melhor como essa função funciona!

A ideia desse método é permitir que você faça consultas flexíveis no seu DAL, aplicando filtros (com Where) e também carregando relacionamentos (com Include) de forma dinâmica.

Veja como construir a função completa no seu DAL:


public IQueryable<T> FindBy(
    Expression<Func<T, bool>>? filtro = null,
    params Expression<Func<T, object?>>[] includes)
{
    IQueryable<T> query = _dbSet;

    // Aplica os includes (relacionamentos)
    foreach (var include in includes)
    {
        query = query.Include(include);
    }

    // Aplica o filtro, se houver
    if (filtro is not null)
    {
        query = query.Where(filtro);
    }

    return query;
}

O que esse método faz:

  • Se você quiser apenas listar tudo, sem filtro e sem include, chama assim:

var artistas = dalArtista.FindBy().ToList();
  • Se quiser buscar um artista específico com suas avaliações:

var artista = dalArtista
    .FindBy(a => a.Id == 1, a => a.Avaliacoes)
    .FirstOrDefault();
  • Se quiser buscar músicas trazendo também o artista relacionado:

var musica = dalMusica
    .FindBy(m => m.Id == 10, m => m.Artista)
    .FirstOrDefault();

Por que isso é útil?

  • O filtro (Expression<Func<T, bool>> filtro) permite que você defina qualquer condição para buscar registros.
  • O params includes permite carregar qualquer relacionamento que desejar.
  • Como o retorno é um IQueryable, você pode continuar encadeando filtros, ordenações, paginação, etc., antes de executar no banco (ou seja, antes do ToList, FirstOrDefault...).

Dessa forma, seu DAL fica muito mais flexível, limpo e performático.

Fico à disposição.

Interessante! E assim como o include ele já me trás o registro com os relacionamentos carregados, certo?

Eu ainda não tinha visto essa forma de filtragem. Obrigado

Oi, Igor!

Exatamente! Ao utilizar essa abordagem com Include no método FindBy, os relacionamentos são carregados automaticamente junto com o registro principal. Isso acontece porque o Include instrui o Entity Framework a gerar a query SQL já trazendo os dados das tabelas relacionadas via JOIN.

Veja este exemplo com a sua função genérica:


var artista = dalArtista
    .FindBy(a => a.Id == 1, a => a.Avaliacoes)
    .FirstOrDefault();

Essa consulta acima já traz o Artista com sua coleção de Avaliacoes populada, diretamente do banco.

Ou seja:

  • Você não precisa de uma segunda consulta separada para buscar as avaliações.
  • Pode acessar diretamente artista.Avaliacoes com os dados já carregados.
  • Isso evita os problemas que você enfrentou, como a lista vir vazia e a verificação FirstOrDefault(...) retornar null.

Além disso, essa forma é muito mais performática e te dá controle total da query — tanto no filtro (Where) quanto nos relacionamentos (Include).

Fico à disposição.