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! 😉
- 01 - Database Schema (DB)
- 02 - WebAPI Project created via CLI
- 03 - Models Creation (Types of DB Schema)
- 04 - Estabilish Relationships using Entity Framework Pattern
- 05 - Entity Framework (EF) Setup
- 06 - Create DTOs based on Models
- 07 - Using AutoMapper to Map DTOs
- 08 - Seeds & Migrations
- 09 - Repository (to encapsulate DB Queries)
- 10 - GET (Read Methods)
- 10.1 - Include in the Interface of the IRepositoryModel the GET methods
- 10.2 - Repository GetAll => _context.Table.ToList()
- 10.3 - Repository GetById => _context.Table.Where(m => m.Id == Id).firstOrDefault()
- 10.4 - Repository EntityExists => _context.Table.Any(m => m.Id == Id)
- 10.5 - Build Other Layers, GET Controller Example with AutoMapper Lib to Map the DTOs
- 11 - POST (Create Methods)
- 12 - PUT (Update Methods)
- 13 - DELETE (Delete Methods)
- 14 - Authentication & Authorization
- 15 - External APIs Requests
dotnet new webapi –name MyWebAPIName
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; }
}
}
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
}
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)
}
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
}
}
Get the connection string
- 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
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);
}
}
}
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
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; }
}
}
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
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();
}
}
}
The recommended steps in order to build any method are basically:
-
- Start modifying the Interface of the repository, adding the methods
-
- Implement the repository and its interface's methods.
-
- Create your DTOs helpers
-
- 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!
}
}
Simply add to the Repository's Interface new contracts for the methods to follow.
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,
}
});
}
Default Simple GetById
public Entity GetEntityById(int EntityId)
{
return _context.Entities.FirstOrDefault(entity => entity.EntityId == EntityId);
}
Default EntityExists
public bool EntityExists(int entityId)
{
return _context.Entities.Any(entity => entity.Id == entityId);
}
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);
}
- ASP.NET Core Web API - 6. GET & Read Methods [PART 1]
- ASP.NET Core Web API - 7. GET & Read Methods [PART 2]
- ASP.NET Core Web API - 8. GET & Read Methods [PART 3]
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
}
};
}
Save
public bool Save()
{
var saved = _context.SaveChanges();
return saved > 0 ? true : false;
}
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");
}
- ASP.NET Core Web API - 9. POST & Create Methods [PART 1]
- ASP.NET Core Web API - 10. POST & Create Methods [PART 2]
Default UpdateEntity
public EntityDto UpdateEntity(Entity entity)
{
_context.Entities.Update(entity);
Save();
return new EntityDto {
EntityId = entity.EntityId,
Prop1 = entity.Prop1,
};
}
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();
}
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();
}
Default DeleteEntities
public bool DeleteEntities(List<Entity> entities)
{
_context.RemoveRange(entities);
return Save();
}
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();
}
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/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();
}
}
}
}
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
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);
}
}
}
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"]
install dotnet add package Swashbuckle.AspNetCore --version 6.2.3
- teddysmithdev for Youtube References
- lucasKoyama for the creation of this Gist
- Danilo Oliveira Silva for review of this Gist