Conectando com um banco de dados

Os endpoints que escrevemos no capítulo anterior não fazem nada além de devolver os dados exatamente como você os envia na requisição. Para que possamos fazer com que estes dados sejam persistidos (ou seja, registrados em algum lugar onde possamos consultá-los futuramente), vamos utilizar um banco de dados. Neste capítulo, vamos criar a comunicação entre o nosso projeto e um gerenciador de banco de dados MariaDB, um fork open-source do MySQL criado e mantido pelos seus desenvolvedores originais.

Vamos utilizar um banco de dados previamente criado para persistir e consultar livros. Para acessá-lo a partir do nosso projeto, precisamos de uma biblioteca. Como o MariaDB é um fork do MySQL, os dois são normalmente intercambiáveis. Bibliotecas desenvolvidas para o MySQL quase sempre irão funcionar com o MariaDB, com exceção de algumas funcionalidades específicas. A documentação do MariaDB recomenda a utilização da biblioteca MySqlConnector, portanto, iremos instalá-la no nosso projeto.

MySqlConnector

Abra o terminal no seu projeto, e execute o comando:

cd Biblioteca
dotnet add package MySqlConnector

Note que o comando precisa ser executado no diretório onde existe o arquivo Biblioteca.csproj. Para confirmar se a biblioteca foi adicionada corretamente, adicione a seguinte linha no topo do seu Program.cs.

using MySqlConnector;

Se o Visual Studio Code não mostra nenhum erro, então a instalação foi bem sucedida.

Modelo de dados

O banco de dados que vamos utilizar possui uma tabela com o nome Livro. Esta tabela possui os seguintes campos:

Id           BIGINT       PRIMARY KEY NOT NULL AUTO_INCREMENT,
Isbn         VARCHAR(255) NOT NULL,
Titulo       VARCHAR(512) NOT NULL,
Autor        TEXT         NOT NULL,
Genero       TEXT         NOT NULL,
Descricao    TEXT         NOT NULL,
Foto         TEXT         NOT NULL,
Keywords     TEXT         NOT NULL,
Ativo        BOOLEAN      NOT NULL DEFAULT 0,
CriadoEm     DATETIME     NOT NULL,
AtualizadoEm DATETIME     NOT NULL,

Classe de modelo e repositório

Vamos criar duas novas classes no projeto: uma classe Livro que irá espelhar a tabela no banco de dados, e uma classe LivroRepository que irá possuir métodos para a consulta e manipulação dos registros nessa tabela. Mas antes disso, vamos limpar o Program.cs, removendo partes do código que escrevemos anteriormente apenas para testar, e que não vamos precisar mais, deixando-o pronto para utilizarmos as novas classes que iremos criar.

Apague todo o conteúdo do seu Program.cs e deixe-o exatamente assim:

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

// Obtém uma lista com os livros registrados.
app.MapGet("/livros", () =>
{
    
});

// Cria um novo livro.
app.MapPost("/livros", () =>
{
    
});

// Edita um livro.
app.MapPut("/livros/{id}", () =>
{
    
});

// Obtém os dados de um livro individual.
app.MapGet("/livros/{id}", () =>
{
    
});

// Remove um livro.
app.MapDelete("/livros/{id}", () =>
{
    
});

app.Run();

Tendo feito isso, crie uma pasta Biblioteca/Models e, dentro dela, crie um arquivo Livro.cs. Dentro deste arquivo, vamos criar uma classe Livro com campos que refletem os campos na tabela do banco de dados.

namespace Biblioteca.Models;

public class Livro
{
    public long Id { get; set; }
    public string? Isbn { get; set; }
    public string? Titulo { get; set; }
    public string? Autor { get; set; }
    public string? Genero { get; set; }
    public string? Descricao { get; set; }
    public string? Foto { get; set; }
    public string? Keywords { get; set; }
    public bool Ativo { get; set; }
    public DateTime CriadoEm { get; set; }
    public DateTime AtualizadoEm { get; set; }
}

Agora, crie uma pasta Biblioteca/Repositories e, dentro dela, crie um arquivo LivroRepository.cs, onde existirá a classe LivroRepository.

namespace Biblioteca.Repositories;

public class LivroRepository
{

}

Nesta classe, vamos precisar de uma constante que vai conter uma string de conexão, que servirá para indicar para o MySqlConnector o endereço e as credenciais do banco de dados que vamos utilizar.

namespace Biblioteca.Repositories;

public class LivroRepository
{
    private const string ConnString = "Server=gbrl.dev;Port=5306;User ID=sistemasdistribuidos.aluno;Password=eW03avS7M8kOUL1A9bZWW2RTIfzEI1Di;Database=sistemasdistribuidos";
}

Listagem de livros

Vamos escrever nesta classe métodos para as operações de listagem, consulta, inclusão, edição e remoção de livros na base de dados. Vamos começar pela operação de listagem.

public async Task<IEnumerable<Livro>> Obter(int pagina)
{

}

Este método é público (public), assíncrono (async Task), retorna um IEnumerable<Livro>, e recebe como parâmetro um valor inteiro que representa a página da consulta

Um método público é um método que pode ser executado por qualquer outro método no projeto. O nosso método precisa ser público pois ele vai ser chamado nos endpoints que estão no Program.cs.

O valor de retorno será um IEnumerable<Livro>. Um IEnumerable é uma interface que representa qualquer sequência iterável de objetos. Por exemplo, um objeto List<T> ou um vetor T[] são iteráveis, portanto podem ser retornados neste método.

Um método assíncrono é um método que pode ser executado assíncronamente, de forma independente do fluxo de execução do método que o chamou. Nós não vamos utilizar nenhuma forma de paralelismo, mas precisamos que nosso método seja assíncrono mesmo assim.

O parâmetro int pagina recebido indica a página da consulta. Você provavelmente já usou algum site onde interagiu com uma listagem de um conteúdo que era feita através de páginas, a ideia aqui é a mesma. Imagine que temos três milhões de livros na base de dados, nós não queremos que nosso método leia todos eles. Ao invés disso, vamos retornar os livros de forma paginada, com N livros por página.

Agora, vamos implementar o método, que ficará assim:

public async Task<IEnumerable<Livro>> Obter(int pagina)
{
    using var conn = new MySqlConnection(ConnString);
    using var cmd  = conn.CreateCommand();

    await conn.OpenAsync();

    var take   = 30;
    var offset = take * Math.Max(pagina-1, 0);
    var lista  = new List<Livro>();

    cmd.CommandText = "SELECT Id, Isbn, Titulo, Autor, Genero, Descricao, Foto, Keywords, Ativo, CriadoEm, AtualizadoEm FROM Livro ORDER BY CriadoEm LIMIT @offset,@take";
    cmd.Parameters.AddWithValue("offset", offset);
    cmd.Parameters.AddWithValue("take", take);

    using var reader = await cmd.ExecuteReaderAsync();

    while (await reader.ReadAsync())
    {
        lista.Add(new()
        {
            Id           = reader.GetInt64(0),
            Isbn         = reader.GetString(1),
            Titulo       = reader.GetString(2),
            Autor        = reader.GetString(3),
            Genero       = reader.GetString(4),
            Descricao    = reader.GetString(5),
            Foto         = reader.GetString(6),
            Keywords     = reader.GetString(7),
            Ativo        = reader.GetBoolean(8),
            CriadoEm     = reader.GetDateTime(9),
            AtualizadoEm = reader.GetDateTime(10),
        });
    }

    return lista;
}

Vamos analisar cada trecho do método.

using var conn = new MySqlConnection(ConnString);
using var cmd  = conn.CreateCommand();

await conn.OpenAsync();

As variáveis conn e cmd representam, respectivamente, a conexão com o banco de dados, e o comando que iremos executar. A declaração de ambas é feita com using pois estes dois objetos alocam recursos que devem ser liberados ao fim da execução do método. O using é uma construção da linguagem que garante que isso aconteça sempre.

Com await conn.OpenAsync() fazemos com que a conexão com o banco de dados seja aberta.

Note que conn.OpenAsync() também é um método assíncrono (pois ele retorna Task). Por isso, devemos prefixar a chamada deste método com um await. Isto é uma indicação de que queremos aguardar a conclusão de conn.OpenAsync() antes de prosseguirmos com a execução do nosso método. E para utilizarmos um await, o nosso método deve ser também, obrigatoriamente, assíncrono. Por isso o declaramos como async Task.

O modelo de programação assíncrona é uma característica que está presente em quase toda a linguagem, mas não precisamos nos aprofundar nela por enquanto. Basta saber que devemos utilizar await em métodos assíncronos, e que se o utilizarmos, o nosso método deve ser também assíncrono, sendo declarado com async Task.

var take   = 30;
var offset = take * Math.Max(pagina-1, 0);
var lista  = new List<Livro>();

Em seguida criamos três variáveis. take é o número de livros que queremos obter por página. offset é um número que indica quantos registros pular para obter a página desejada. Por exemplo, se desejamos a primeira página, devemos obter os n primeiros livros. Já se quisermos a segunda página, vamos pular n livros e depois obter os n próximos. Generalizando, para obter a página p (considerando que as páginas começam do 1), com n elementos, precisamos pular n(p-1) livros.

Como int pagina é um valor inteiro que pode assumir valores negativos, fazemos Math.Max(pagina-1, 0) como uma garantia para que offset nunca tenha um valor menor do que zero.

A variável lista é uma lista de objetos Livro que irá guardar todos os resultados da consulta, e depois será retornada.

cmd.CommandText = "SELECT Id, Isbn, Titulo, Autor, Genero, Descricao, Foto, Keywords, Ativo, CriadoEm, AtualizadoEm FROM Livro ORDER BY CriadoEm LIMIT @offset,@take";
cmd.Parameters.AddWithValue("offset", offset);
cmd.Parameters.AddWithValue("take", take);

A primeira linha adiciona a consulta que queremos executar à variável cmd. As duas linhas seguintes adicionam take e offset como parâmetros da consulta.

using var reader = await cmd.ExecuteReaderAsync();

Enfim, executamos a consulta através de cmd.ExecuteReaderAsync(). Este método é assíncrono, portanto deve ser prefixado por await, e além disso também aloca recursos que devem ser liberados com using. O retorno desta função é um objeto onde podemos ler os resultados da consulta.

while (await reader.ReadAsync())
{
    lista.Add(new()
    {
        Id           = reader.GetInt64(0),
        Isbn         = reader.GetString(1),
        Titulo       = reader.GetString(2),
        Autor        = reader.GetString(3),
        Genero       = reader.GetString(4),
        Descricao    = reader.GetString(5),
        Foto         = reader.GetString(6),
        Keywords     = reader.GetString(7),
        Ativo        = reader.GetBoolean(8),
        CriadoEm     = reader.GetDateTime(9),
        AtualizadoEm = reader.GetDateTime(10),
    });
}

Este laço é repetido enquanto await reader.ReadAsync() retornar true, o que significa que existe um resultado da consulta que pode ser lido. Quando isso acontece, podemos ler as colunas do resultado através dos métodos de reader, como reader.GetString(0). O número passado como parâmetro para estas funções indica a posição da coluna lida. Por exemplo, na consulta que realizamos (SELECT Id, Isbn, Titulo, ...), a coluna Id é a primeira, portanto, para ler este valor, devemos executar reader.GetInt64(0) (Int64, um número inteiro de 64-bits, é um sinônimo para long). Isbn é a segunda, então o seu valor é obtido através de reader.GetString(1), e assim sucessivamente.

Realizamos a leitura de todas as colunas do resultado, e montamos um objeto do tipo Livro, o qual adicionamos à lista lista, que é o valor de retorno do método de listagem.

return lista;

Para testarmos este método, vamos chamá-lo no endpoint de listagem em Program.cs

// Obtém uma lista com os livros registrados.
app.MapGet("/livros", async () =>
{
    var repo = new LivroRepository();
    var res  = await repo.Obter(pagina: 1);

    return res;
});

Como LivroRepository.Obter(int) é assíncrono, o endpoint também deve ser. Fazemos isso adicionando async na declaração da sua função.

app.MapGet("/livros", async () => ...

Agora execute este endpoint. Se tudo deu certo, você verá alguns livros cadastrados no retorno.

Obtendo um livro pela chave primária

O Id é a chave primária da tabela. Vamos implementar um método que consulta um único livro a partir deste dado.

public async Task<Livro> Obter(long id)
{
    using var conn = new MySqlConnection(ConnString);
    using var cmd  = conn.CreateCommand();

    await conn.OpenAsync();

    cmd.CommandText = "SELECT Id, Isbn, Titulo, Autor, Genero, Descricao, Foto, Keywords, Ativo, CriadoEm, AtualizadoEm FROM Livro WHERE Id=@id";
    cmd.Parameters.AddWithValue("id", id);

    using var reader = await cmd.ExecuteReaderAsync();

    var existe = await reader.ReadAsync();

    if (!existe)
    {
        throw new Exception($"Livro {id} não encontrado");
    }

    return new()
    {
        Id           = reader.GetInt64(0),
        Isbn         = reader.GetString(1),
        Titulo       = reader.GetString(2),
        Autor        = reader.GetString(3),
        Genero       = reader.GetString(4),
        Descricao    = reader.GetString(5),
        Foto         = reader.GetString(6),
        Keywords     = reader.GetString(7),
        Ativo        = reader.GetBoolean(8),
        CriadoEm     = reader.GetDateTime(9),
        AtualizadoEm = reader.GetDateTime(10),
    };
}

As três primeiras linhas deste método, assim como no de listagem, abre uma conexão com o banco de dados.

cmd.CommandText = "SELECT Id, Isbn, Titulo, Autor, Genero, Descricao, Foto, Keywords, Ativo, CriadoEm, AtualizadoEm FROM Livro WHERE Id=@id";
cmd.Parameters.AddWithValue("id", id);

A consulta é montada na variável cmd, o parâmetro id é adicionado ao comando.

using var reader = await cmd.ExecuteReaderAsync();

var existe = await reader.ReadAsync();

if (!existe)
{
    throw new Exception($"Livro {id} não encontrado");
}

return new()
{
    Id           = reader.GetInt64(0),
    Isbn         = reader.GetString(1),
    Titulo       = reader.GetString(2),
    Autor        = reader.GetString(3),
    Genero       = reader.GetString(4),
    Descricao    = reader.GetString(5),
    Foto         = reader.GetString(6),
    Keywords     = reader.GetString(7),
    Ativo        = reader.GetBoolean(8),
    CriadoEm     = reader.GetDateTime(9),
    AtualizadoEm = reader.GetDateTime(10),
};

Assim como na listagem, await cmd.ExecuteReaderAsync() é chamado para ler os resultados da consulta. Como esperamos que só haja um registro com aquele Id, executamos await reader.ReadAsync() apenas uma vez. Se este método retorna false, é por que nenhum registro foi encontrado com esta chave primária. Neste caso, lançamos um erro. Do contrário, utilizamos os métodos de leitura do registro lido para montar um objeto Livro que será retornado.

Vamos chamar este novo método ao endpoint de consulta no Program.cs.

// Obtém os dados de um livro individual.
app.MapGet("/livros/{id}", async (long id) =>
{
    var repo = new LivroRepository();
    var res  = await repo.Obter(id);

    return res;
});

Criando livros

Vamos implementar agora o método de criação de livros:

public async Task<Livro> Criar(Livro dados)
{

}

Este método recebe um objeto Livro como parâmetro, contendo os dados do novo livro que deve ser inserido no banco de dados. Vamos implementá-lo.

public async Task<Livro> Criar(Livro dados)
{
    using var conn = new MySqlConnection(ConnString);
    using var cmd  = conn.CreateCommand();

    await conn.OpenAsync();

    var livro = new Livro
    {
        Isbn         = dados.Isbn?.Trim() ?? "",
        Titulo       = dados.Titulo?.Trim() ?? "",
        Autor        = dados.Autor?.Trim() ?? "",
        Genero       = dados.Genero?.Trim() ?? "",
        Descricao    = dados.Descricao?.Trim() ?? "",
        Foto         = dados.Foto?.Trim() ?? "",
        Keywords     = dados.Keywords?.Trim() ?? "",
        Ativo        = true,
        CriadoEm     = DateTime.Now,
        AtualizadoEm = default,
    };

    if (livro.Titulo == "")
    {
        throw new Exception("O título do livro é obrigatório.");
    }

    cmd.CommandText =
        @"
        INSERT INTO Livro
        (Isbn, Titulo, Autor, Genero, Descricao, Foto, Keywords, Ativo, CriadoEm, AtualizadoEm)
        VALUES
        (@isbn, @titulo, @autor, @genero, @descricao, @foto, @keywords, @ativo, @criadoem, @atualizadoem)
        ";

    cmd.Parameters.AddWithValue("isbn",         livro.Isbn);
    cmd.Parameters.AddWithValue("titulo",       livro.Titulo);
    cmd.Parameters.AddWithValue("autor",        livro.Autor);
    cmd.Parameters.AddWithValue("genero",       livro.Genero);
    cmd.Parameters.AddWithValue("descricao",    livro.Descricao);
    cmd.Parameters.AddWithValue("foto",         livro.Foto);
    cmd.Parameters.AddWithValue("keywords",     livro.Keywords);
    cmd.Parameters.AddWithValue("ativo",        livro.Ativo);
    cmd.Parameters.AddWithValue("criadoem",     livro.CriadoEm);
    cmd.Parameters.AddWithValue("atualizadoem", livro.AtualizadoEm);

    await cmd.ExecuteNonQueryAsync();

    using var cmd2 = conn.CreateCommand();

    cmd2.CommandText = "SELECT LAST_INSERT_ID()";

    using var reader = await cmd2.ExecuteReaderAsync();

    await reader.ReadAsync();

    livro.Id = reader.GetInt64(0);

    return livro;
}

O método se inicia com a abertura da conexão com o banco de dados.

var livro = new Livro
{
    Isbn         = dados.Isbn?.Trim() ?? "",
    Titulo       = dados.Titulo?.Trim() ?? "",
    Autor        = dados.Autor?.Trim() ?? "",
    Genero       = dados.Genero?.Trim() ?? "",
    Descricao    = dados.Descricao?.Trim() ?? "",
    Foto         = dados.Foto?.Trim() ?? "",
    Keywords     = dados.Keywords?.Trim() ?? "",
    Ativo        = true,
    CriadoEm     = DateTime.Now,
    AtualizadoEm = default,
};

Criamos uma variável que será uma cópia do objeto recebido no parâmetro. Fazemos isso para tratar os dados do livro, formatando-os, e assegurando que os seus campos assumam os valores corretos. Este objeto é contém os dados que serão enviados para o banco de dados.

O símbolo ? é o operador de propagação de nulos. Em uma cadeia de expressões de acesso de membros em um objeto, um propagador de nulo faz com que, se um membro for nulo, o resultado de toda a expressão seja nula. Por exemplo, dados.Descricao é um valor do tipo string, que pode assumir um valor nulo. Na expressão dados.Descricao.Trim(), se dados.Descricao for nulo, um NullReferenceException será lançado, pois não é possível executar um método ou acessar uma propriedade em um objeto nulo. Já na expressão dados.Descricao?.Trim(), se dados.Descricao for nulo, como há o operador de propagação de nulos, .Trim() não será executado, e o resultado da expressão será null.

De forma semelhante, o ?? age sobre dois operandos. Na expressão var x = A ?? B, a variável x irá assumir o valor de A apenas se ela não for nula. Do contrário, x assumirá o valor de B.

Portanto, na expressão dados.Descricao?.Trim() ?? "", se dados.Descricao for nulo, então a expressão ficará null ?? "", que terá como resultado a string vazia "". Ou seja, Se dados.Descricao for nulo, livro.Descricao será uma string vazia, e nunca assumirá null.

Fazemos isso nos campos do objeto livro para garantir que nenhum deles seja nulo, já que no banco de dados, todos os campos da tabela são NOT NULL.

if (livro.Titulo == "")
{
    throw new Exception("O título do livro é obrigatório.");
}

Fazemos algumas validações básicas, para assegurar que os dados obrigatórios do registro tenham sido preenchidos.

cmd.CommandText =
    @"
    INSERT INTO Livro
    (Isbn, Titulo, Autor, Genero, Descricao, Foto, Keywords, Ativo, CriadoEm, AtualizadoEm)
    VALUES
    (@isbn, @titulo, @autor, @genero, @descricao, @foto, @keywords, @ativo, @criadoem, @atualizadoem)
    ";

cmd.Parameters.AddWithValue("isbn",         livro.Isbn);
cmd.Parameters.AddWithValue("titulo",       livro.Titulo);
cmd.Parameters.AddWithValue("autor",        livro.Autor);
cmd.Parameters.AddWithValue("genero",       livro.Genero);
cmd.Parameters.AddWithValue("descricao",    livro.Descricao);
cmd.Parameters.AddWithValue("foto",         livro.Foto);
cmd.Parameters.AddWithValue("keywords",     livro.Keywords);
cmd.Parameters.AddWithValue("ativo",        livro.Ativo);
cmd.Parameters.AddWithValue("criadoem",     livro.CriadoEm);
cmd.Parameters.AddWithValue("atualizadoem", livro.AtualizadoEm);

await cmd.ExecuteNonQueryAsync()

Assim como no método de listagem e da consulta por chave primária, montamos o SQL com um INSERT, adicionamos os parâmetros do comando, com os dados do objeto livro, e depois o executamos.

using var cmd2 = conn.CreateCommand();

cmd2.CommandText = "SELECT LAST_INSERT_ID()";

using var reader = await cmd2.ExecuteReaderAsync();

await reader.ReadAsync();

livro.Id = reader.GetInt64(0);

return livro;

Em seguida, executamos uma consulta para obter o id do livro recém criado, e o atribuímos à livro.Id, para que possamos incluir este dado, a chave primária e identificador do livro criado, na resposta do endpoint.

Adicionando-o ao endpoint de criação, temos:

// Cria um novo livro.
app.MapPost("/livros", async (Livro livro) =>
{
    var repo = new LivroRepository();
    var res  = await repo.Criar(livro);

    return res;
});

Editando um livro

O método de edição é similar ao de criação.

public async Task<Livro> Editar(long id, Livro dados)
{
    using var conn = new MySqlConnection(ConnString);
    using var cmd  = conn.CreateCommand();

    await conn.OpenAsync();

    var livro = await Obter(id);

    livro.Titulo       = dados.Titulo?.Trim() ?? "";
    livro.Autor        = dados.Autor?.Trim() ?? "";
    livro.Genero       = dados.Genero?.Trim() ?? "";
    livro.Descricao    = dados.Descricao?.Trim() ?? "";
    livro.Foto         = dados.Foto?.Trim() ?? "";
    livro.Keywords     = dados.Keywords?.Trim() ?? "";
    livro.AtualizadoEm = DateTime.Now;

    cmd.CommandText =
        @"
        UPDATE Livro SET
        Titulo=@titulo, Autor=@autor, Genero=@genero, Descricao=@descricao, Foto=@foto, Keywords=@keywords, AtualizadoEm=@atualizadoem
        WHERE Id=@id
        ";

    cmd.Parameters.AddWithValue("id",           id);
    cmd.Parameters.AddWithValue("titulo",       livro.Titulo);
    cmd.Parameters.AddWithValue("autor",        livro.Autor);
    cmd.Parameters.AddWithValue("genero",       livro.Genero);
    cmd.Parameters.AddWithValue("descricao",    livro.Descricao);
    cmd.Parameters.AddWithValue("foto",         livro.Foto);
    cmd.Parameters.AddWithValue("keywords",     livro.Keywords);
    cmd.Parameters.AddWithValue("atualizadoem", livro.AtualizadoEm);

    await cmd.ExecuteNonQueryAsync();

    return livro;
}

O método recebe dois parâmetros, um isbn que identifica o livro que será editado, e um objeto com os novos dados do livro.

var livro = await Obter(id);

livro.Titulo       = dados.Titulo?.Trim() ?? "";
livro.Autor        = dados.Autor?.Trim() ?? "";
livro.Genero       = dados.Genero?.Trim() ?? "";
livro.Descricao    = dados.Descricao?.Trim() ?? "";
livro.Foto         = dados.Foto?.Trim() ?? "";
livro.Keywords     = dados.Keywords?.Trim() ?? "";
livro.AtualizadoEm = DateTime.Now;

Chamamos a função Obter(long) que criamos para obter do banco de dados o livro que será editado. Em seguida, atribuímos a este objeto os valores do objeto dados, formatando-os devidamente.

cmd.CommandText =
    @"
    UPDATE Livro SET
    Titulo=@titulo, Autor=@autor, Genero=@genero, Descricao=@descricao, Foto=@foto, Keywords=@keywords, AtualizadoEm=@atualizadoem
    WHERE Id=@id
    ";

cmd.Parameters.AddWithValue("id",           id);
cmd.Parameters.AddWithValue("titulo",       livro.Titulo);
cmd.Parameters.AddWithValue("autor",        livro.Autor);
cmd.Parameters.AddWithValue("genero",       livro.Genero);
cmd.Parameters.AddWithValue("descricao",    livro.Descricao);
cmd.Parameters.AddWithValue("foto",         livro.Foto);
cmd.Parameters.AddWithValue("keywords",     livro.Keywords);
cmd.Parameters.AddWithValue("atualizadoem", livro.AtualizadoEm);

await cmd.ExecuteNonQueryAsync();

return livro;

Em seguida, montamos o comando um UPDATE, atribuímos os parâmetros, executamos a consulta e retornamos o livro que foi editado.

Vamos agora chamar este método no seu endpoint.

// Edita um livro.
app.MapPut("/livros/{id}", async (long id, Livro livro) =>
{
    var repo = new LivroRepository();
    var res  = await repo.Editar(id, livro);

    return res;
});

Removendo um livro

Nós não iremos remover livros do banco de dados, ao invés disso, a operação de remoção irá editar apenas o campo Ativo de um livro, alterando-o para false. Portanto, o método de remover será basicamente uma edição.

public async Task Desativar(long id)
{
    using var conn = new MySqlConnection(ConnString);
    using var cmd  = conn.CreateCommand();

    await conn.OpenAsync();

    cmd.CommandText = "UPDATE Livro SET Ativo=false WHERE Id=@id";
    cmd.Parameters.AddWithValue("id", id);

    await cmd.ExecuteNonQueryAsync();
}

Vamos adicioná-lo agora ao seu endpoint.

// Remove um livro.
app.MapDelete("/livros/{id}", async (long id) =>
{
    var repo = new LivroRepository();

    await repo.Desativar(id);
});

Finalizando

Implementamos todas as operações básicas de manipulação dos registros, e os adicionamos aos endpoints da nossa API. Mais ainda precisamos ajustar algumas coisas.

  • O endpoint de listagem está sempre obtendo a primeira página da consulta. Tente fazer seu endpoint receber um parâmetro de url (como /livros?pagina=1), para que seja possível obter as próximas páginas além da primeira.
  • Observe que, na listagem, os livros que foram removidos (ou seja, que estão com Ativo=false) ainda aparecem. Não queremos que isso aconteça, queremos que os livros inativos não apareçam. Como você poderia fazer isso?

O código-fonte do projeto desenvolvido até agora pode ser baixado aqui.