DEV Community

Cover image for C# como padrão de desenvolvimento de API's Web (CRUD com Mongo + Auth)
Gabriel Grubba
Gabriel Grubba

Posted on • Originally published at blog-grubba.vercel.app

3 1

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

csharp

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')
Enter fullscreen mode Exit fullscreen mode

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"
  },
Enter fullscreen mode Exit fullscreen mode

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; }
    }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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; }
}
Enter fullscreen mode Exit fullscreen mode

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; }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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>();
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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";
}

Enter fullscreen mode Exit fullscreen mode

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);
    }
}

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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
        };
    }
}

Enter fullscreen mode Exit fullscreen mode

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;
    }
Enter fullscreen mode Exit fullscreen mode

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.

AWS GenAI LIVE image

Real challenges. Real solutions. Real talk.

From technical discussions to philosophical debates, AWS and AWS Partners examine the impact and evolution of gen AI.

Learn more

Top comments (0)

The Most Contextual AI Development Assistant

Pieces.app image

Our centralized storage agent works on-device, unifying various developer tools to proactively capture and enrich useful materials, streamline collaboration, and solve complex problems through a contextual understanding of your unique workflow.

👥 Ideal for solo developers, teams, and cross-company projects

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay