03 de abril de 2022 • 12 min de leitura
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
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 ?
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.