C# como padrão de desenvolvimento de API's Web (CRUD com Mongo + Auth)

Nesse blogpost vou falar sobre esse projeto CRUD + JWT auth com C# e como a organização que o .NET nos trás deveria ser o padrão de desenvolvimento web

C# .net

Introdução

Antes de começar vou deixar o link do repo desse projeto de onde tiro os códigos aqui dispostos no blogpost repo.

Também gostaria de deixar o link do driver do mongo para caso queira fazer em ambiente local é necessario ter o mongo instalado em sua maquina baixe o mongo.

Por onde começar ?

Depois de ter criado o seu banco de dados em mongo e a coleção na qual será feito o crud, no caso dos dois CRUD que eu fiz eu criei duas coleções uma de livros e outra de pessoas, no terminal do mongo eu rodei respectivamente os comandos:

db.createCollection('Books')
db.createCollection('Users')

dando certo a criação das collections, adicione as configurações em seu

// no arquivo appsettings.json

  "LibraryDatabase": {
    "ConnectionString": "mongodb://localhost:27017",
    "DatabaseName": "Library", // Nome do DB
    "BooksCollectionName": "Books", // Nome das coleções conforme o comando acima
    "UsersCollectionName": "Users"
  },

arquivo appsettings.json

Crie também uma classe onde irá conter as configurações que você colocar em seu JSON:

    public class MongoDBSettings
    {
        public string ConnectionString { get; set; }
        public string DataBaseName { get; set; }
        public string BooksCollectionName { get; set; }
        public string UsersCollectionName { get; set; }
    }

por fim tabém é necessario fazer a injeção de dependência no seu programa com essas variavéis de ambiente, isso é feito no Program.cs

builder.Services.Configure<MongoDBSettings>(
    builder.Configuration.GetSection("LibraryDatabase")); // Pega a seção do config conforme o seu nome

Com o projeto configurado, você deve estar se perguntando como que irá ser feito a modelagem?

Modelando os dados

Usar o mongo, em questão de desenvolvimento, sua produtividade é ímpar. Para criar o modelo de dados de Livro como desenvolvedor o que precisa ser feito é criar uma model que nada mais é que uma classe onde irá representar esses dados.

using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;

namespace WebApplicationCRUDExample.Models;

public class Book
{
    [BsonId]
    [BsonRepresentation(BsonType.ObjectId)]
    public string Id { get; set; }

    public string Name { get; set; }

    public string Author { get; set; }

    public string Summary { get; set; }

    public string CoverURL { get; set; }

    public string Category { get; set; }

    public decimal Price { get; set; }
}

Model de Livro

a Model de User não é muito diferente:

using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;

namespace WebApplicationCRUDExample.Models;

public class User
{
    [BsonId]
    [BsonRepresentation(BsonType.ObjectId)]
    public string? Id { get; set; }

    public string Name { get; set; }

    public List<string>? UserLikes { get; set; }
}

Model de User

Criando os Services

Um bom padrão a se seguir é usar services para conectar com os dados no banco, de forma que cada classe terá apenas uma única responsabilidade.

o Service de livros, chamado de Library ficou dessa forma:

using Microsoft.Extensions.Options;
using MongoDB.Driver;
using WebApplicationCRUDExample.Models;
using WebApplicationCRUDExample.Services.DB;

namespace WebApplicationCRUDExample.Services;

public class LibraryService
{
    private readonly IMongoCollection<Book> _booksCollection;

    public LibraryService(
        IOptions<MongoDBSettings> library)
    {
        var mongoClient = new MongoClient(
            library.Value.ConnectionString);

        var mongoDatabase = mongoClient.GetDatabase(
            library.Value.DataBaseName);

        _booksCollection = mongoDatabase.GetCollection<Book>(
            library.Value.BooksCollectionName);
    }

    public async Task<List<Book>> GetBookAsync()
    {
        return await _booksCollection.Find(_ => true).ToListAsync();
    }

    public async Task<Book?> GetBookByIdAsync(string id)
    {
        return await _booksCollection.Find(x => x.Id == id).FirstOrDefaultAsync();
    }

    public async Task CreateBookAsync(Book newBook)
    {
        await _booksCollection.InsertOneAsync(newBook);
    }

    public async Task UpdateBookAsync(string id, Book updatedBook)
    {
        await _booksCollection.ReplaceOneAsync(x => x.Id == id, updatedBook);
    }

    public async Task RemoveBookAsync(string id)
    {
        await _booksCollection.DeleteOneAsync(x => x.Id == id);
    }
}

LibraryService.cs

É simplesmente o uso do Driver do mongo para fazer alterações.

Se algum dia mudar o Schema da model, estará tudo ok, uma vez que a service só faz a ponte entre o programa e o banco.

Já a service de User terá uma estrutura extremamente parecida:

using Microsoft.Extensions.Options;
using MongoDB.Driver;
using WebApplicationCRUDExample.Models;
using WebApplicationCRUDExample.Services.DB;

namespace WebApplicationCRUDExample.Services;

public class UserService
{
    private readonly IMongoCollection<User> _usersCollection;

    public UserService(
        IOptions<MongoDBSettings> library)
    {
        var mongoClient = new MongoClient(
            library.Value.ConnectionString);

        var mongoDatabase = mongoClient.GetDatabase(
            library.Value.DataBaseName);

        _usersCollection = mongoDatabase.GetCollection<User>(
            library.Value.UsersCollectionName);
    }

    public async Task<List<User>> GetUserAsync()
    {
        return await _usersCollection.Find(_ => true).ToListAsync();
    }

    public async Task<User?> GetUserByIdAsync(string id)
    {
        return await _usersCollection.Find(x => x.Id == id).FirstOrDefaultAsync();
    }

    public async Task CreateUserAsync(User newUser)
    {
        await _usersCollection.InsertOneAsync(newUser);
    }

    public async Task UpdateUserAsync(string id, User updatedUser)
    {
        await _usersCollection.ReplaceOneAsync(x => x.Id == id, updatedUser);
    }

    public async Task RemoveUserAsync(string id)
    {
        await _usersCollection.DeleteOneAsync(x => x.Id == id);
    }
}

UserService.cs

Como todo serviço é usado com injeção de dependencia de uma controller, é preciso declarar no builder também, eu fiz da seguinte forma no Program.cs :

builder.Services.AddSingleton<LibraryService>();
builder.Services.AddSingleton<UserService>();

Program.cs

Criando as Controllers

Criamos o banco, a ponte do banco com o programa, agora vamos criar o front do progama, a parte que se conecta com o mundo externo. A controller de Library ficará assim:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using WebApplicationCRUDExample.Models;
using WebApplicationCRUDExample.Services;

namespace WebApplicationCRUDExample.Controllers;

[ApiController]
[Route("api/[controller]")]
public class LibraryController : Controller
{
    private readonly LibraryService _libraryService;

    public LibraryController(LibraryService libraryService)
    {
        _libraryService = libraryService;
    }


    [HttpGet("/books")]
    [Authorize] // já irei explicar o que é authorize
    public async Task<List<Book>> GetBooks()
    {
        return await _libraryService.GetBookAsync();
    }

    [HttpGet("/books/{id:length(24)}")]
    [Authorize]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [ProducesResponseType(StatusCodes.Status200OK)]
    public async Task<ActionResult<Book>> GetBookById(string id)
    {
        var book = await _libraryService.GetBookByIdAsync(id);

        if (book is null) return NotFound();

        return book;
    }

    [HttpPost("/books/")]
    [Authorize]
    [ProducesResponseType(StatusCodes.Status201Created)]
    public async Task<IActionResult> PostBook(Book book)
    {
        await _libraryService.CreateBookAsync(book);
        return CreatedAtAction(nameof(GetBookById), new {id = book.Id}, book);
    }

    [HttpPut("/books/{id:length(24)}")]
    [Authorize]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    public async Task<IActionResult> UpdateBook(string id, Book updatedBook)
    {
        var oldBook = await _libraryService.GetBookByIdAsync(id);

        if (oldBook is null) return NotFound();

        await _libraryService.UpdateBookAsync(id, updatedBook);

        return NoContent();
    }

    [HttpDelete("/books/{id:length(24)}")]
    [Authorize]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    public async Task<IActionResult> DeleteBook(string id)
    {
        var book = await _libraryService.GetBookByIdAsync(id);

        if (book is null) return NotFound();

        await _libraryService.RemoveBookAsync(id);

        return NoContent();
    }
}

LibraryController.cs

Se você puder observar, nessa api usamos a route api/books/.... Por ser um crud sem regras de negócio, o objetivo é ser o mais simples o possivel.

O que pode gerar dúvida é a função CreatedAtAction(nameof(GetBookById), new {id = book.Id}, book) que faz a função de dar um get na hora do post. Pois o driver do mongo do C# não tem a opção de retornar o registro novo.

o Controller de User também é bem parecido:

using Microsoft.AspNetCore.Mvc;
using WebApplicationCRUDExample.Models;
using WebApplicationCRUDExample.Services;

namespace WebApplicationCRUDExample.Controllers;

[ApiController]
[Route("api/[controller]")]
public class UserController : Controller
{
    private readonly UserService _userService;
    private readonly LibraryService _libraryService;

    public UserController(UserService userService, LibraryService libraryService)
    {
        _userService = userService;
        _libraryService = libraryService;
    }


    [HttpGet("/users/")]
    public async Task<List<User>> GetUsers()
    {
        return await _userService.GetUserAsync();
    }

    [HttpGet("/users/{id:length(24)}")]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [ProducesResponseType(StatusCodes.Status200OK)]
    public async Task<ActionResult<User>> GetUserById(string id)
    {
        var user = await _userService.GetUserByIdAsync(id);

        if (user is null) return NotFound();

        return user;
    }

    [HttpGet("/users/{id:length(24)}/likes")]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [ProducesResponseType(StatusCodes.Status200OK)]
    public async Task<ActionResult<List<Book>>> GetUserLikes(string id)
    {
        var user = await _userService.GetUserByIdAsync(id);
        var bookList = new List<Book>();

        if (user is null) return NotFound();

        if (user.UserLikes is null) return BadRequest();

        foreach (var bookId in user.UserLikes)
        {
            var book = await _libraryService.GetBookByIdAsync(bookId);
            if (book is not null) bookList.Add(book);
        }

        return bookList;
    }

    [HttpPost("/users/")]
    [ProducesResponseType(StatusCodes.Status201Created)]
    public async Task<IActionResult> PostUser(User user)
    {
        await _userService.CreateUserAsync(user);
        return CreatedAtAction(nameof(GetUserById), new {id = user.Id}, user);
    }

    [HttpPut("/users/{id:length(24)}")]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    public async Task<IActionResult> UpdateUser(string id, User updatedUser)
    {
        var oldUser = await _userService.GetUserByIdAsync(id);

        if (oldUser is null) return NotFound();

        await _userService.UpdateUserAsync(id, updatedUser);
        return NoContent();
    }


    [HttpDelete("/users/{id:length(24)}")]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    public async Task<IActionResult> DeleteUser(string id)
    {
        var user = await _userService.GetUserByIdAsync(id);

        if (user is null) return NotFound();

        await _userService.RemoveUserAsync(id);

        return NoContent();
    }
}

UserController.cs

Se você em Library Controller não usar o decorator [Authorize], você já poderá testar e também poderá ver que o relacionamento de user likes funciona, se você passar os ID's dos livros, um user pode 'curtir' outros livros.

Como colocar auth com JWT em meus serviços ?

cadeado

Para se criar Auth é necessario baixar os pacotes Nuget


dotnet add package Microsoft.AspNetCore.Authentication
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

Depois disso crie uma chave que será a password de encriptação do seu JWT.É recomendado usar o appsettings.json para isso mas para mostrar outra forma que também é possivel de se configurar sua aplicação, iremos usar o modelo de uma classe estática de Settings. info detalhada da MS sobre como pegar dados do json de config.

A classe ficará dessa forma no nosso caso:


public static class Settings
{
    public static string Secret = "FeWENgwGTUe2vz5Vtfnc64MrwkeNM56D";
}

Agora para usar, podemos fazer como nesse caso aqui em nosso serviço Estático de Auth:


public static class AuthService
{
    public static string GenerateToken(User user)
    {
        var tokenHandler = new JwtSecurityTokenHandler();
        var key = Encoding.ASCII.GetBytes(Settings.Secret);
        var tokenDescriptor = new SecurityTokenDescriptor
        {
            Subject = new ClaimsIdentity(new Claim[]
            {
                new Claim(ClaimTypes.Name, user.Name),
            }),
            Expires = DateTime.UtcNow.AddHours(24),
            SigningCredentials =
                new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
        };
        var token = tokenHandler.CreateToken(tokenDescriptor);
        return tokenHandler.WriteToken(token);
    }
}

Por ser um serviço estático não precisamos adicionar no Program.cs , mas precisamos adicionar no swagger a possibilidade de colocar o bearer no nosso header de testes.

Será necessario adicionar algumas configurações em seu Program.cs.

Para isso ficarmos alinhados como está o Program.cs, segue ele abaixo:


using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using WebApplicationCRUDExample;
using WebApplicationCRUDExample.Services;
using WebApplicationCRUDExample.Services.DB;

#region Builder

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.Configure<MongoDBSettings>(
    builder.Configuration.GetSection("LibraryDatabase"));

builder.Services.AddSingleton<LibraryService>();
builder.Services.AddSingleton<UserService>();

builder.Services.AddEndpointsApiExplorer();

builder.Services.AddSwaggerGen(setup =>
{
    // Include 'SecurityScheme' to use JWT Authentication
    var jwtSecurityScheme = new OpenApiSecurityScheme
    {
        Scheme = "bearer",
        BearerFormat = "JWT",
        Name = "JWT Authentication",
        In = ParameterLocation.Header,
        Type = SecuritySchemeType.Http,
        Description = "Put **_ONLY_** your JWT Bearer token on textbox below!",

        Reference = new OpenApiReference
        {
            Id = JwtBearerDefaults.AuthenticationScheme,
            Type = ReferenceType.SecurityScheme
        }
    };

    setup.AddSecurityDefinition(jwtSecurityScheme.Reference.Id, jwtSecurityScheme);

    setup.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {jwtSecurityScheme, Array.Empty<string>()}
    });
});

var key = Encoding.ASCII.GetBytes(Settings.Secret);
builder.Services
    .AddAuthentication(auth =>
    {
        auth.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
        auth.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(bearer =>
        {
            bearer.RequireHttpsMetadata = false;
            bearer.SaveToken = true;
            bearer.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(key),
                ValidateIssuer = false,
                ValidateAudience = false
            };
        }
    );

#endregion

#region App

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();
app.Run();

#endregion

Por fim para usar o auth, é só criarmos uma controller de login onde poderemos passar por todo esse processo:


using Microsoft.AspNetCore.Mvc;
using WebApplicationCRUDExample.Services;

namespace WebApplicationCRUDExample.Controllers;

[ApiController]
[Route("api/[controller]")]
public class AuthController : Controller
{
    private readonly UserService _userService;

    public AuthController(UserService userService)
    {
        _userService = userService;
    }


    [HttpPost("/auth/")]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<ActionResult<dynamic>> Authenticate([FromBody] string id)
    {
        // Obtem esse id via email/hash e usa para logar o user;

        var user = await _userService.GetUserByIdAsync(id);

        if (user is null) return NotFound();

        var token = AuthService.GenerateToken(user);

        return new
        {
            user, token
        };
    }
}

No caso, por ser apenas uma PoC eu deixei como forma de login, os Id's baterem, é uma forma usada em aplicações onde para logar, o user precisa entrar no email dele e fazer o Two-factor.

Mas facilmente poderia ser feito da forma convencional de email e senha.

Usando o Auth

Agora a parte interessante, como o projeto está configurado e já temos uma route de autenticação, podemos usar o [Authorize] em rotas que precisam estar autenticadas, se o usuario não estiver por padrão o .NET irá retornar um não autorizado, como no exemplo a seguir:


 [HttpGet("/books/{id:length(24)}")]
    [Authorize] // precisa estar logado
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [ProducesResponseType(StatusCodes.Status200OK)]
    public async Task<ActionResult<Book>> GetBookById(string id)
    {
        var book = await _libraryService.GetBookByIdAsync(id);

        if (book is null) return NotFound();

        return book;
    }

Muito legal não é ?

Toda essa parte de auth eu fiz seguindo esse tutorial muito bom disponibilizado pelo André Baltieri em seu blog. Caso queiram ver a fonte inicial está aqui

Próximos passos

Nesse projeto onde continuarei a explicar e escrever sobre algumas das maravilhas do C#, irei na proxima vez explicar um pouco sobre testes nesse nosso crud e como poderiamos fazer os testes.

Contato

Se quiser discutir sobre qualquer assunto ou viu algum erro, não hesite em me marcar ou chamar no Twitter: @que_cara_legal
Estou sempre tentando trazer o que tenho estudado, as vezes traduzindo algums tópicos divertidos que me chamam atenção.