Skip to content

Instantly share code, notes, and snippets.

@lucasKoyama
Last active March 8, 2024 23:01
Show Gist options
  • Save lucasKoyama/2078263386f130516e2a5b778a0b073e to your computer and use it in GitHub Desktop.
Save lucasKoyama/2078263386f130516e2a5b778a0b073e to your computer and use it in GitHub Desktop.
REST API + CRUD + .NET 6 + EF + Authentication + Authorization + Login + External API Requests

Web API => REST API + CRUD + .NET 6 + EF + Authentication + Authorization + Login + External API Requests

The purpose of this gist is to provide a step-by-step guide on how to create a REST API with all CRUD methods using .NET 6 and Entity Framework.

💡 Additionally, you can fork this gist and use the summary checkboxes to mark the steps as finished!

💡 Every Default Method will have a boilerplate code to replace the Entity1/Entity2/Entities names accordintly to your Table or Columns names!

❗ This is a step-by-step Cheat Sheet, so I won't dive deep into explanations of technical terms, just code snippets!❗

😉 But referencial links to public explanations will be provided! 😉

Summary

Database Schema (DB)

Refs:

WebAPI Project created via CLI

dotnet new webapi –name MyWebAPIName

Refs:

Models Creation (Types of DB Schema)

Models are the database's tables representation in code

Here I've listed for example a few models, the full list is here

Pokemon.cs
using System.ComponentModel.DataAnnotations;

namespace PokemonReviewApp.Models
{
    public class Pokemon
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public DateTime BirthDate { get; set; }
        public ICollection<Review> Reviews { get; set; }
        public ICollection<PokemonOwner> PokemonOwners { get; set; }
        public ICollection<PokemonCategory> PokemonCategories { get; set; }
    }
}
Review.cs
namespace PokemonReviewApp.Models
{
    public class Review
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string Text { get; set; }
        public int Rating { get; set; }
        public Reviewer Reviewer { get; set; }
        public Pokemon Pokemon { get; set; }
    }
}
Reviewer.cs
namespace PokemonReviewApp.Models
{
    public class Reviewer
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public ICollection<Review> Reviews { get; set; }
    }
}

Refs:

Estabilish Relationships using Entity Framework Pattern

1:1

Default Model 1:1
public class Entity1
{
    public int Entity1Id { get; set; }
    public string prop1 { get; set; }

    public virtual Entity2 Entity2 { get; set; } // 1-Entity1:1-Entity2
}
public class Entity2
{
    [Key, ForeignKey("Entity1")]
    public int Entity1Id { get; set; }
    public string prop1 { get; set; }

    public virtual Entity1 Entity1 { get; set; } // 1-Entity2:1-Entity1
}

1:N

Default Model 1:N
namespace YourApp.Models;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

public class Entity1 {
  [Key]
  public int Entity1Id { get; set; }
  public string prop1 { get; set; }

  [ForeignKey("Entity2Id")]
  public int Entity2Id { get; set; } // Entity2 is a ForeignKey
  public virtual Entity2 Entity2 { get; set; } = null!; // (N-Entity1:1-Entity2) Entity2 is a table, virtual is used to represent the table for later a future "join" 
}
namespace YourApp.Models;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

public class Entity2 {
  [Key]
  public int Entity2Id { get; set; }
  public string? prop1 { get; set; }

  public virtual ICollection<Entity1>? Entities1 { get; set; } = null!; // (1-Entity2:N-Entity1)
}

1:N Refs:

N:N

N:N
namespace PokemonReviewApp.Models
{
    public class PokemonOwner
    {
        public int PokemonId { get; set; }
        public int OwnerId { get; set; }
        public Pokemon Pokemon { get; set; } // N-Pokemons associated with N-Owners
        public Owner Owner { get; set; }     // N-Pokemon:N-Owner
    }

    public class Pokemon
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public DateTime BirthDate { get; set; }
        public ICollection<Review> Reviews { get; set; }
        public ICollection<PokemonOwner> PokemonOwners { get; set; } // 1-Pokemon:N-PokemonOwners
        public ICollection<PokemonCategory> PokemonCategories { get; set; }
    }

    public class Owner
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Gym { get; set; }
        public Country Country { get; set; }
        public ICollection<PokemonOwner> PokemonOwners { get; set; } // 1-Owner:N-PokemonOwners
    }
}

N:N Refs:

Entity Framework (EF) Setup

Connection String

Get the connection string

EF + Provider of DB installed

image

  • If you don't have the EF CLI, install it
    dotnet tool install --global dotnet-ef --version 7.0.4
  • install Entity Framework
    dotnet add package Microsoft.EntityFrameworkCore 7.0.10
  • install your provider, in this Example using SqlServer
    dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 7.0.10
  • To generate migrations and entities via CLI
    dotnet add package Microsoft.EntityFrameworkCore.Design --version 7.0.10
  • To use migrations
    dotnet add package Microsoft.EntityFrameworkCore.Tools --version 7.0.10

Create the “ctx” to represent the Database’s tables

Default context
using Microsoft.EntityFrameworkCore;
using YourApp.Models;

namespace YourApp.Data
{
    public class DataContext : DbContext
    {
        public DataContext(DbContextOptions<DataContext> options) : base(options) { }

        public DbSet<Model> TableName { get; set; } // Default database's table representation
        // Dealing with N:N below
        public DbSet<Entity2> Entities2 { get; set; }
        public DbSet<Entity1> Entity1 { get; set; }
        public DbSet<Entity1Entity2> Entity1Entities2 { get; set; } // N-Entity1:N-Entity2

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            if (!optionsBuilder.IsConfigured)
            {
                var connectionString = "your connection string";
                optionsBuilder.UseSqlServer(connectionString);
            }
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            // N:N goes here!
            modelBuilder.Entity<Entity1Entity2>()
                    .HasKey(pk => new { pk.Entity1Id, pk.Entity2Id }); // Primary Key
            modelBuilder.Entity<Entity1Entity2>()
                    .HasOne(e1 => e1.Entity1)
                    .WithMany(e1e2 => e1e2.Entity1Entities2)
                    .HasForeignKey(e1 => e1.Entity1Id);
            modelBuilder.Entity<Entity1Entity2>()
                    .HasOne(e2 => e2.Entity2)
                    .WithMany(e1e2 => e1e2.Entity1Entities2)
                    .HasForeignKey(e2 => e2.Entity2Id);
        }
    }
}
Pokemon App Example Context
using Microsoft.EntityFrameworkCore;
using PokemonReviewApp.Models;

namespace PokemonReviewApp.Data
{
    public class DataContext : DbContext
    {
        public DataContext(DbContextOptions<DataContext> options) : base(options)
        {

        }

        public DbSet<Category> Categories { get; set; }
        public DbSet<Country> Countries { get; set; }
        public DbSet<Owner> Owners { get; set; }
        public DbSet<Pokemon> Pokemon { get; set; }
        public DbSet<PokemonOwner> PokemonOwners { get; set; }
        public DbSet<PokemonCategory> PokemonCategories { get; set; }
        public DbSet<Review> Reviews { get; set; }
        public DbSet<Reviewer> Reviewers { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<PokemonCategory>()
                    .HasKey(pc => new { pc.PokemonId, pc.CategoryId });
            modelBuilder.Entity<PokemonCategory>()
                    .HasOne(p => p.Pokemon)
                    .WithMany(pc => pc.PokemonCategories)
                    .HasForeignKey(p => p.PokemonId);
            modelBuilder.Entity<PokemonCategory>()
                    .HasOne(p => p.Category)
                    .WithMany(pc => pc.PokemonCategories)
                    .HasForeignKey(c => c.CategoryId);

            modelBuilder.Entity<PokemonOwner>()
                    .HasKey(po => new { po.PokemonId, po.OwnerId });
            modelBuilder.Entity<PokemonOwner>()
                    .HasOne(p => p.Pokemon)
                    .WithMany(pc => pc.PokemonOwners)
                    .HasForeignKey(p => p.PokemonId);
            modelBuilder.Entity<PokemonOwner>()
                    .HasOne(p => p.Owner)
                    .WithMany(pc => pc.PokemonOwners)
                    .HasForeignKey(c => c.OwnerId);
        }
    }
}

Refs:

Seeds & Migrations

Remember, the main functionality of a migration is to migrate all the current DB data to a new format with new tables, columns and things like that.

After the Context created with the Models the only thing needed to generate the migrations automatically is to run the add migration command and the update command. Below is a list of migration commands:

  • Create a migration
    dotnet ef migrations add MigrationName
  • Execute migration
    dotnet ef database update
  • List existing migrations
    dotnet ef migrations list
  • Remove last migration
    dotnet ef migrations remove
  • Create a script based on migrations
    dotnet ef migrations script -o nome-do-arquivo.sql
  • Create a script based on a starting and ending migration
    dotnet ef database update destinyMigration
  • Revert a migration
    dotnet ef database update destinyMigration
  • Remove and Revert a migration
    dotnet ef migrations remove --force

Create DTOs based on Models

Default DTO
namespace YourApp.Dto
{
    public class ModelDto
    {
        public int Id { get; set; }
        public string Prop1 { get; set; }
    }
}
PokemonDTO compare this with the Pokemon Model
namespace PokemonReviewApp.Dto
{
    public class PokemonDto
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public DateTime BirthDate { get; set; }
    }
}

Using AutoMapper to Map DTOs

install AutoMapper Libs:

  • dotnet add package AutoMapper --version 11.0.0
  • dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection --version 11.0.0
Default MappingProfiles
using AutoMapper;
using YourApp.Dto;
using YourApp.Models;

namespace YourApp.Utils
{
    public class MappingProfiles : Profile
    {
        public MappingProfiles()
        {
            CreateMap<Model1, Model1Dto>();
            CreateMap<Model1Dto, Model1>();
            CreateMap<Model2, Model2Dto>();
            CreateMap<Model2Dto, Model2>();
        }
    }
}
Pokemon App Example MappingProfiles
using AutoMapper;
using PokemonReviewApp.Dto;
using PokemonReviewApp.Models;

namespace PokemonReviewApp.Helper
{
    public class MappingProfiles : Profile
    {
        public MappingProfiles()
        {
            CreateMap<Pokemon, PokemonDto>();
            CreateMap<Category, CategoryDto>();
            CreateMap<CategoryDto, Category>();
            CreateMap<CountryDto, Country>();
            CreateMap<OwnerDto, Owner>();
            CreateMap<PokemonDto, Pokemon>();
            CreateMap<ReviewDto, Review>();
            CreateMap<ReviewerDto, Reviewer>();
            CreateMap<Country, CountryDto>();
            CreateMap<Owner, OwnerDto>();
            CreateMap<Review, ReviewDto>();
            CreateMap<Reviewer, ReviewerDto>();
        }
    }
}
Add to Program.cs
builder.Services.AddControllers().AddJsonOptions(x => x.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles); // avoid infinite loop in N:N
builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); // implement AutoMapper

Repository (to encapsulate DB Queries)

In this layer, the DataContext will be used to handle with the database operations, and all our queries will be encapsulated here to abstract the queries logic into simple and descriptive database manipulation methods names.

Default Repository
using AutoMapper;
using YourApp.Data;
using YourApp.Interfaces;
using YourApp.Models;

namespace YourApp.Repository
{
    public class YourAppRepository : IYourAppRepository
    {
        private readonly DataContext _context;
        private readonly IMapper _mapper;

        public YourAppRepository(DataContext context, IMapper mapper)
        {
            _context = context;
            _mapper = mapper;
        }

        // Methods goes below!

    }
}
IReviewRepository example
using PokemonReviewApp.Models;

namespace PokemonReviewApp.Interfaces
{
    public interface IReviewRepository
    {
        ICollection<Review> GetReviews();
        Review GetReview(int reviewId);
        ICollection<Review> GetReviewsOfAPokemon(int pokeId);
        bool ReviewExists(int reviewId);
        bool CreateReview(Review review);
        bool UpdateReview(Review review);
        bool DeleteReview(Review review);
        bool DeleteReviews(List<Review> reviews);
        bool Save();
    }
}
ReviewRepository fully implemented example
using AutoMapper;
using PokemonReviewApp.Data;
using PokemonReviewApp.Interfaces;
using PokemonReviewApp.Models;

namespace PokemonReviewApp.Repository
{
    public class ReviewRepository : IReviewRepository
    {
        private readonly DataContext _context;
        private readonly IMapper _mapper;

        public ReviewRepository(DataContext context, IMapper mapper)
        {
            _context = context;
            _mapper = mapper;
        }

        public bool CreateReview(Review review)
        {
            _context.Add(review);
            return Save();
        }

        public bool DeleteReview(Review review)
        {
          _context.Remove(review);
            return Save();
        }

        public bool DeleteReviews(List<Review> reviews)
        {
            _context.RemoveRange(reviews);
            return Save();
        }

        public Review GetReview(int reviewId)
        {
            return _context.Reviews.Where(r => r.Id == reviewId).FirstOrDefault();
        }

        public ICollection<Review> GetReviews()
        {
            return _context.Reviews.ToList();
        }

        public ICollection<Review> GetReviewsOfAPokemon(int pokeId)
        {
            return _context.Reviews.Where(r => r.Pokemon.Id == pokeId).ToList();
        }

        public bool ReviewExists(int reviewId)
        {
            return _context.Reviews.Any(r => r.Id == reviewId);
        }

        public bool Save()
        {
            var saved = _context.SaveChanges();
            return saved > 0 ? true : false;
        }

        public bool UpdateReview(Review review)
        {
            _context.Update(review);
            return Save();
        }
    }
}

Refs:

Understanding the steps to build CRUD Methods in a Layered Architecture

The recommended steps in order to build any method are basically:

    1. Start modifying the Interface of the repository, adding the methods
    1. Implement the repository and its interface's methods.
    1. Create your DTOs helpers
    1. Build Other layers, like controllers and services using the repository and DTOs built previously
Default Controller
using AutoMapper;
using Microsoft.AspNetCore.Mvc;
using YourApp.Dto;
using YourApp.Interfaces;
using YourApp.Models;

namespace YourApp.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ReviewController : Controller
    {
        private readonly IReviewRepository _reviewRepository;
        private readonly IMapper _mapper;
        private readonly IReviewerRepository _reviewerRepository;
        private readonly IPokemonRepository _pokemonRepository;

        public ReviewController(IReviewRepository reviewRepository, 
            IMapper mapper,
            IPokemonRepository pokemonRepository,
            IReviewerRepository reviewerRepository)
        {
            _reviewRepository = reviewRepository;
            _mapper = mapper;
            _reviewerRepository = reviewerRepository;
            _pokemonRepository = pokemonRepository;
        }

        // Methods goes below!

    }
}

GET (Read Methods)

Include in the Interface of the IRepositoryModel the GET methods

Simply add to the Repository's Interface new contracts for the methods to follow.

Repository GetAll => _context.Table.ToList()

Default Simple GetAll
public IEnumerable<EntityDto> GetEntities()
{
    return _context.Entities.Select(entity => (
        new EntityDto { 
            EntityId = entity.EntityId,
            Prop1 = entity.Prop1
        }
    ));
}
Default Nested GetAll
public IEnumerable<Entity1Dto> GetEntities1(int Entity2Id)
{
    return _context.Entities1
        .Include(entity1 => entity1.Entity2) // Join into Entities1 Entity2
        .ThenInclude(entity2 => entity2.Entity3) // Join into Entities1 Entity3
        .Where(entity1 => entity1.Entity2Id == Entity2Id) // Filtered By Entity2Id
        .Select(entity1 => new Entity1Dto { // Mapping to returned DTO
            Entity1Id = entity1.Entity1Id,
            Prop1 = entity1.Prop1,
            Entity2 = new Entity2Dto {
                Entity2Id = entity1.Entity2Id,
                Prop1 = entity1.Entity2.Prop1,
                Prop2 = entity1.Entity2.Entity3.Prop2,
            }
        });
}

Repository GetById => _context.Table.Where(m => m.Id == Id).firstOrDefault()

Default Simple GetById
public Entity GetEntityById(int EntityId)
{
    return _context.Entities.FirstOrDefault(entity => entity.EntityId == EntityId);
}

Repository EntityExists => _context.Table.Any(m => m.Id == Id)

Default EntityExists
public bool EntityExists(int entityId)
{
    return _context.Entities.Any(entity => entity.Id == entityId);
}

Build Other Layers, GET Controller Example with AutoMapper Lib to Map the DTOs

Default Simple GetAll Filtering By Entity2Id
[HttpGet("{Entity2Id}")]
public IActionResult GetEntity(int Entity2Id)
{
    return Ok(_repository.GetEntities(Entity2Id));
}
Simple GetAll + AutoMapper
[HttpGet]
[ProducesResponseType(200, Type = typeof(IEnumerable<Review>))]
public IActionResult GetReviews()
{
    var reviews = _mapper.Map<List<ReviewDto>>(_reviewRepository.GetReviews());

    if (!ModelState.IsValid)
        return BadRequest(ModelState);

    return Ok(reviews);
}
Default Simple GetById
[HttpGet("{Entityid}")]
public IActionResult GetEntity(int Entityid){
    try
    {
        return Ok(_repository.GetEntity(Entityid));
    }
    catch (NullReferenceException err)
    {
        return NotFound(new { message = err.Message });
    }
}
Nested GetAll
[HttpGet("pokemon/{pokeId}")]
[ProducesResponseType(200, Type = typeof(Review))]
[ProducesResponseType(400)]
public IActionResult GetReviewsForAPokemon(int pokeId)
{
    var reviews = _mapper.Map<List<ReviewDto>>(_reviewRepository.GetReviewsOfAPokemon(pokeId));

    if (!ModelState.IsValid)
        return BadRequest();

    return Ok(reviews);
}

Refs:

POST (Create Methods)

Repository CreateEntity => _context.Add(entity)

Default Simple CreateEntity
public EntityDto AddEntity1(Entity1 entity1)
{
    _context.Entities1.Add(entity1);
    Save();
    return new EntityDto {
        EntityId = entity1.EntityId,
        Prop1 = entity1.Prop1
    };
}
Default Nested CreateEntity
public Entity1Dto AddEntity1(Entity1 entity1) {
    _context.Entities1.Add(entity1);
    Save();
    var newEntity1 = _context.Entities1
        .Include(entity1 => entity1.Entity2)
        .ThenInclude(entity2 => entity2.Entity3)
        .Where(entity1Item => entity1Item.Entity1Id == entity1.Entity1Id)
        .Single();

    return new Entity1Dto {
        Entity1Id = newEntity1.Entity1Id,
        Prop1 = newEntity1.Prop1,
        Entity2 = new Entity2Dto {
            Entity2Id = newEntity1.Entity2Id,
            Prop1 = newEntity1.Entity2.Prop1,
            Entity3Id = newEntity1.Entity2.CityId,
            Entity3Prop1 = newEntity1.Entity2.Entity3.Prop1
        }
    };
}

Repository Save => _context.SaveChanges() > 0 ? true : false

Save
public bool Save()
{
    var saved = _context.SaveChanges();
    return saved > 0 ? true : false;
}

POST Controller Example

Default Simple/Nested CreateEntity
[HttpPost]
public IActionResult PostEntity([FromBody] Entity entity)
{
    return Created("", _repository.AddEntity(entity));
}
CreateEntity
[HttpPost]
[ProducesResponseType(204)]
[ProducesResponseType(400)]
public IActionResult CreateReview([FromQuery] int reviewerId, [FromQuery]int pokeId, [FromBody] ReviewDto reviewCreate)
{
    if (reviewCreate == null)
        return BadRequest(ModelState);

    var reviews = _reviewRepository.GetReviews()
        .Where(c => c.Title.Trim().ToUpper() == reviewCreate.Title.TrimEnd().ToUpper())
        .FirstOrDefault();

    if (reviews != null)
    {
        ModelState.AddModelError("", "Review already exists");
        return StatusCode(422, ModelState);
    }

    if (!ModelState.IsValid)
        return BadRequest(ModelState);

    var reviewMap = _mapper.Map<Review>(reviewCreate);

    reviewMap.Pokemon = _pokemonRepository.GetPokemon(pokeId);
    reviewMap.Reviewer = _reviewerRepository.GetReviewer(reviewerId);
    

    if (!_reviewRepository.CreateReview(reviewMap))
    {
        ModelState.AddModelError("", "Something went wrong while savin");
        return StatusCode(500, ModelState);
    }

    return Ok("Successfully created");
}

Refs:

PUT (Update Methods)

Repository UpdateEntity => _context.Update(entity)

Default UpdateEntity
public EntityDto UpdateEntity(Entity entity)
{
   _context.Entities.Update(entity);
   Save();
   return new EntityDto {
      EntityId = entity.EntityId,
      Prop1 = entity.Prop1,
   };
}

PUT Controller Example

UpdateEntity
[HttpPut("{reviewId}")]
[ProducesResponseType(400)]
[ProducesResponseType(204)]
[ProducesResponseType(404)]
public IActionResult UpdateReview(int reviewId, [FromBody] ReviewDto updatedReview)
{
    if (updatedReview == null)
        return BadRequest(ModelState);

    if (reviewId != updatedReview.Id)
        return BadRequest(ModelState);

    if (!_reviewRepository.ReviewExists(reviewId))
        return NotFound();

    if (!ModelState.IsValid)
        return BadRequest();

    var reviewMap = _mapper.Map<Review>(updatedReview);

    if (!_reviewRepository.UpdateReview(reviewMap))
    {
        ModelState.AddModelError("", "Something went wrong updating review");
        return StatusCode(500, ModelState);
    }

    return NoContent();
}

Refs:

DELETE (Delete Methods)

Repository DeleteEntity => _context.Remove(entity)

Default Nested DeleteEntity
public void DeleteEntity1(int Entity1Id) {
    var entity1 = _context.Entities1
        .Include(entity1 => entity1.Entity2)
        .Single(entity1 => entity1.Entity1Id == Entity1Id);
    _context.Entities1.Remove(entity1);
    Save();
}

Repository DeleteEntities => _context.RemoveRange(entities)

Default DeleteEntities
public bool DeleteEntities(List<Entity> entities)
{
    _context.RemoveRange(entities);
    return Save();
}

DELETE Controller Example

Default DeleteEntity
[HttpDelete("{EntityId}")]
public IActionResult Delete(int EntityId)
{
    _repository.DeleteEntity(EntityId);
    return NoContent();
}
DeleteEntity
[HttpDelete("{reviewId}")]
[ProducesResponseType(400)]
[ProducesResponseType(204)]
[ProducesResponseType(404)]
public IActionResult DeleteReview(int reviewId)
{
    if (!_reviewRepository.ReviewExists(reviewId))
    {
        return NotFound();
    }

    var reviewToDelete = _reviewRepository.GetReview(reviewId);

    if (!ModelState.IsValid)
        return BadRequest(ModelState);

    if (!_reviewRepository.DeleteReview(reviewToDelete))
    {
        ModelState.AddModelError("", "Something went wrong deleting owner");
    }

    return NoContent();
}
DeleteEntities
// Added missing delete range of reviews by a reviewer **>CK
[HttpDelete("/DeleteReviewsByReviewer/{reviewerId}")]
[ProducesResponseType(400)]
[ProducesResponseType(204)]
[ProducesResponseType(404)]
public IActionResult DeleteReviewsByReviewer(int reviewerId)
{
    if (!_reviewerRepository.ReviewerExists(reviewerId))
        return NotFound();

    var reviewsToDelete = _reviewerRepository.GetReviewsByReviewer(reviewerId).ToList();
    if (!ModelState.IsValid)
        return BadRequest();

    if (!_reviewRepository.DeleteReviews(reviewsToDelete))
    {
        ModelState.AddModelError("", "error deleting reviews");
        return StatusCode(500, ModelState);
    }
    return NoContent();
}

Refs:

Authentication & Authorization

Authentication Setup

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

Default TokenOptions
namespace YourApp.Services
public class TokenOptions
{
    {
        public const string Token = "Token";
        public string Secret { get; set; }
        public int ExpiresDay { get; set; }
    }
}
default TokenGenerator + Claims(Email + Admin Role)
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using YourApp.Models;
using YourApp.Dto;

namespace YourApp.Services
public class TokenGenerator
{
    private readonly TokenOptions _tokenOptions;
    public TokenGenerator()
    {
    _tokenOptions = new TokenOptions
    {
        Secret = "your secret",
        ExpiresDay = 1
    };

    }
    public string Generate(UserDto user)
    {
        var tokenHandler = new JwtSecurityTokenHandler();

        var tokenDescriptor = new SecurityTokenDescriptor()
        {
            Subject = AddClaims(user), // "payload"
            SigningCredentials = new SigningCredentials
            (
                new SymmetricSecurityKey(Encoding.ASCII.GetBytes(_tokenOptions.Secret)),
                SecurityAlgorithms.HmacSha256Signature
            ),
            Expires = DateTime.Now.AddDays(_tokenOptions.ExpiresDay)
        };

        var token = tokenHandler.CreateToken(tokenDescriptor);

        return tokenHandler.WriteToken(token);
    }

    private ClaimsIdentity AddClaims(UserDto user)
    {
        var claims = new ClaimsIdentity();

        claims.AddClaim(new Claim(ClaimTypes.Email, user.Email));
        if (user.UserType == "admin")
            claims.AddClaim(new Claim(ClaimTypes.Role, "admin"));

        return claims;
    }
}

Login + Auth Token JWT

Login/User Repository
public UserDto Login(LoginDto login)
{
    var user = _context.Users
        .FirstOrDefault(u => u.Password == login.Password && u.Email == login.Email);
    if (user is null) throw new InvalidDataException("Incorrect e-mail or password");
    return new UserDto {
        UserId = user.UserId,
        Email = user.Email,
    };
}
LoginController
using Microsoft.AspNetCore.Mvc;
using YourApp.Models;
using YourApp.Repository;
using YourApp.Dto;
using YourApp.Services;
using Microsoft.AspNetCore.Authentication.OAuth;

namespace YourApp.Controllers
{
    [ApiController]
    [Route("login")]

    public class LoginController : Controller
    {

        private readonly IUserRepository _repository;
        public LoginController(IUserRepository repository)
        {
            _repository = repository;
        }

        [HttpPost]
        public IActionResult Login([FromBody] LoginDto login)
        {
            try
            {
                var user = _repository.Login(login);
                var token = new TokenGenerator().Generate(user);
                return Ok(new { token });
            }
            catch (InvalidDataException err)
            {
                return Unauthorized(new { message = err.Message });
            }
            catch (Exception)
            {
                return BadRequest();
            }
        }
    }
}

Authorization Policies

Add to Program.cs
builder.Services.AddAuthorization(options =>
{

    options.AddPolicy("Admin", policy =>
    {
        policy.RequireClaim(ClaimTypes.Email);
        policy.RequireClaim(ClaimTypes.Role);
    });
    options.AddPolicy("Client", policy => policy.RequireClaim(ClaimTypes.Email));
});
Using Authorization Policies in methods
[HttpPost] // Some Http Method
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] // JWT Bearer Authentication
[Authorize(Policy = "Admin")] // Authorization

External APIs Requests

Default IExternalApiNameService (Interface)
public interface IExternalApiNameService
{
  // "object" is used when you don't the type returned from the API, in other words a JSON
  public Task<object> GetAnyJson();

  Task<List<EntitiesResponse>> GetEntities();
}
Default ExternalApiNameService (implementation)
using System.Net.Http;
using System.Text.Json.Serialization;
using FluentAssertions;
using YourApp.Dto;
using YourApp.Repository;

namespace YourApp.Services
{
    public class ExternalApiNameService : IExternalApiNameService
    {
        private readonly HttpClient _client;
        private const string _baseUrl = "https://EXTERNAL-BASE-API-URL/";
        
        public ExternalApiNameService(HttpClient client)
        {
            _client = client;
            _client.BaseAddress = new Uri(_baseUrl);

            // Some APIs require default headers
            _client.DefaultRequestHeaders.Add("Accept", "application/json"); 
            _client.DefaultRequestHeaders.Add("User-Agent", "aspnet-user-agent");
        }

        public async Task<object> GetAnyJson()
        {
            // "https://EXTERNAL-BASE-API-URL/" + "CONTINUE-API-ENDPOINT"
            var response = await _client.GetAsync("CONTINUE-API-ENDPOINT");

            // Failed response default return
            if (!response.IsSuccessStatusCode) return default(Object);

            // Convert the JSON TEXT to the <Type/Entity>
            var result = await response.Content.ReadFromJsonAsync<object>();
            return result;
        }

        public async Task<List<EntitiesResponse>> GetEntities()
        {
            var response = await _client.GetAsync("CONTINUE-API-ENDPOINT");

            // You can throw exceptions instead of returning a default value
            if (!response.IsSuccessStatusCode) return throw new Exception();

            var result = await response.Content.ReadFromJsonAsync<List<EntitiesResponse>>();
            return result;
        }

    }
}
Add to Program.cs
builder.Services.AddHttpClient<IExternalApiNameService, ExternalApiNameService>();
Default Controller With External API Usage
using Microsoft.AspNetCore.Mvc;
using YourApp.Models;
using YourApp.Repository;
using YourApp.Dto;
using YourApp.Services;


namespace YourApp.Controllers
{
    [ApiController]
    [Route("YOUR-ROUTE")]
    public class YourController : Controller
    {
        private readonly IExternalApiNameService _ExternalApiNameService;


        public YourController(IExternalApiNameService externalApiNameService)
        {
            _externalApiNameService = externalApiNameService;
        }

        [HttpGet]
        [Route("YOUR-ENDPOINT")]
        public async Task<IActionResult> GetAnyResponse()
        {
            var anyResponse = await _externalApiNameService.GetAnyJson();

            if (anyResponse is null)
            {
              return NotFound();
            }

            return Ok(anyResponse);
        }

        [HttpGet]
        [Route("YOUR-ENDPOINT")]
        public async Task<IActionResult> GetEntities()
        {
            var entitiesResponse = await _externalApiNameService.GetEntities();

            if (entitiesResponse is null)
            {
              return NotFound();
            }

            return Ok(entitiesResponse);
        }
    }
}

Using docker to deploy!

Default Dockerfile
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build-env
WORKDIR /app

COPY . ./
RUN dotnet restore
RUN dotnet publish -c Release -o out

FROM mcr.microsoft.com/dotnet/aspnet:6.0
WORKDIR /app
COPY --from=build-env /app/out .
ENTRYPOINT ["dotnet", "YOUR-WEBAPI-NAME.dll"]  

Bonus

Swagger

install dotnet add package Swashbuckle.AspNetCore --version 6.2.3

BadRequest

AlreadyExists

Credits and Thanks to:

@delso-ferreira
Copy link

Nice @lucasKoyama , very good content, helping me with my C# studies.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment