├── .gitignore ├── Controllers └── UsersController.cs ├── Entities ├── Role.cs └── User.cs ├── Helpers ├── AppException.cs ├── AutoMapperProfile.cs ├── DataContext.cs └── ErrorHandlerMiddleware.cs ├── LICENSE ├── Models └── Users │ ├── CreateRequest.cs │ └── UpdateRequest.cs ├── Program.cs ├── README.md ├── Services └── UserService.cs ├── Startup.cs ├── WebApi.csproj ├── appsettings.json └── omnisharp.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | typings 33 | 34 | # Optional npm cache directory 35 | .npm 36 | 37 | # Optional REPL history 38 | .node_repl_history 39 | 40 | # .NET compiled files 41 | bin 42 | obj 43 | 44 | # local sqlite db that is auto generated 45 | LocalDatabase.db -------------------------------------------------------------------------------- /Controllers/UsersController.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Microsoft.AspNetCore.Mvc; 3 | using WebApi.Models.Users; 4 | using WebApi.Services; 5 | 6 | namespace WebApi.Controllers 7 | { 8 | [ApiController] 9 | [Route("[controller]")] 10 | public class UsersController : ControllerBase 11 | { 12 | private IUserService _userService; 13 | private IMapper _mapper; 14 | 15 | public UsersController( 16 | IUserService userService, 17 | IMapper mapper) 18 | { 19 | _userService = userService; 20 | _mapper = mapper; 21 | } 22 | 23 | [HttpGet] 24 | public IActionResult GetAll() 25 | { 26 | var users = _userService.GetAll(); 27 | return Ok(users); 28 | } 29 | 30 | [HttpGet("{id}")] 31 | public IActionResult GetById(int id) 32 | { 33 | var user = _userService.GetById(id); 34 | return Ok(user); 35 | } 36 | 37 | [HttpPost] 38 | public IActionResult Create(CreateRequest model) 39 | { 40 | _userService.Create(model); 41 | return Ok(new { message = "User created" }); 42 | } 43 | 44 | [HttpPut("{id}")] 45 | public IActionResult Update(int id, UpdateRequest model) 46 | { 47 | _userService.Update(id, model); 48 | return Ok(new { message = "User updated" }); 49 | } 50 | 51 | [HttpDelete("{id}")] 52 | public IActionResult Delete(int id) 53 | { 54 | _userService.Delete(id); 55 | return Ok(new { message = "User deleted" }); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Entities/Role.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.Entities 2 | { 3 | public enum Role 4 | { 5 | Admin, 6 | User 7 | } 8 | } -------------------------------------------------------------------------------- /Entities/User.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace WebApi.Entities 4 | { 5 | public class User 6 | { 7 | public int Id { get; set; } 8 | public string Title { get; set; } 9 | public string FirstName { get; set; } 10 | public string LastName { get; set; } 11 | public string Email { get; set; } 12 | public Role Role { get; set; } 13 | 14 | [JsonIgnore] 15 | public string PasswordHash { get; set; } 16 | } 17 | } -------------------------------------------------------------------------------- /Helpers/AppException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | 4 | namespace WebApi.Helpers 5 | { 6 | // custom exception class for throwing application specific exceptions (e.g. for validation) 7 | // that can be caught and handled within the application 8 | public class AppException : Exception 9 | { 10 | public AppException() : base() {} 11 | 12 | public AppException(string message) : base(message) { } 13 | 14 | public AppException(string message, params object[] args) 15 | : base(String.Format(CultureInfo.CurrentCulture, message, args)) 16 | { 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /Helpers/AutoMapperProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using WebApi.Entities; 3 | using WebApi.Models.Users; 4 | 5 | namespace WebApi.Helpers 6 | { 7 | public class AutoMapperProfile : Profile 8 | { 9 | public AutoMapperProfile() 10 | { 11 | // CreateRequest -> User 12 | CreateMap(); 13 | 14 | // UpdateRequest -> User 15 | CreateMap() 16 | .ForAllMembers(x => x.Condition( 17 | (src, dest, prop) => 18 | { 19 | // ignore both null & empty string properties 20 | if (prop == null) return false; 21 | if (prop.GetType() == typeof(string) && string.IsNullOrEmpty((string)prop)) return false; 22 | 23 | // ignore null role 24 | if (x.DestinationMember.Name == "Role" && src.Role == null) return false; 25 | 26 | return true; 27 | } 28 | )); 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /Helpers/DataContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.Extensions.Configuration; 3 | using WebApi.Entities; 4 | 5 | namespace WebApi.Helpers 6 | { 7 | public class DataContext : DbContext 8 | { 9 | protected readonly IConfiguration Configuration; 10 | 11 | public DataContext(IConfiguration configuration) 12 | { 13 | Configuration = configuration; 14 | } 15 | 16 | protected override void OnConfiguring(DbContextOptionsBuilder options) 17 | { 18 | // in memory database used for simplicity, change to a real db for production applications 19 | options.UseInMemoryDatabase("TestDb"); 20 | } 21 | 22 | public DbSet Users { get; set; } 23 | } 24 | } -------------------------------------------------------------------------------- /Helpers/ErrorHandlerMiddleware.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.Extensions.Logging; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Net; 6 | using System.Text.Json; 7 | using System.Threading.Tasks; 8 | 9 | namespace WebApi.Helpers 10 | { 11 | public class ErrorHandlerMiddleware 12 | { 13 | private readonly RequestDelegate _next; 14 | private readonly ILogger _logger; 15 | 16 | public ErrorHandlerMiddleware(RequestDelegate next, ILogger logger) 17 | { 18 | _next = next; 19 | _logger = logger; 20 | } 21 | 22 | public async Task Invoke(HttpContext context) 23 | { 24 | try 25 | { 26 | await _next(context); 27 | } 28 | catch (Exception error) 29 | { 30 | var response = context.Response; 31 | response.ContentType = "application/json"; 32 | 33 | switch (error) 34 | { 35 | case AppException e: 36 | // custom application error 37 | response.StatusCode = (int)HttpStatusCode.BadRequest; 38 | break; 39 | case KeyNotFoundException e: 40 | // not found error 41 | response.StatusCode = (int)HttpStatusCode.NotFound; 42 | break; 43 | default: 44 | // unhandled error 45 | _logger.LogError(error, error.Message); 46 | response.StatusCode = (int)HttpStatusCode.InternalServerError; 47 | break; 48 | } 49 | 50 | var result = JsonSerializer.Serialize(new { message = error?.Message }); 51 | await response.WriteAsync(result); 52 | } 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Jason Watmore 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Models/Users/CreateRequest.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using WebApi.Entities; 3 | 4 | namespace WebApi.Models.Users 5 | { 6 | public class CreateRequest 7 | { 8 | [Required] 9 | public string Title { get; set; } 10 | 11 | [Required] 12 | public string FirstName { get; set; } 13 | 14 | [Required] 15 | public string LastName { get; set; } 16 | 17 | [Required] 18 | [EnumDataType(typeof(Role))] 19 | public string Role { get; set; } 20 | 21 | [Required] 22 | [EmailAddress] 23 | public string Email { get; set; } 24 | 25 | [Required] 26 | [MinLength(6)] 27 | public string Password { get; set; } 28 | 29 | [Required] 30 | [Compare("Password")] 31 | public string ConfirmPassword { get; set; } 32 | } 33 | } -------------------------------------------------------------------------------- /Models/Users/UpdateRequest.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using WebApi.Entities; 3 | 4 | namespace WebApi.Models.Users 5 | { 6 | public class UpdateRequest 7 | { 8 | public string Title { get; set; } 9 | public string FirstName { get; set; } 10 | public string LastName { get; set; } 11 | 12 | [EnumDataType(typeof(Role))] 13 | public string Role { get; set; } 14 | 15 | [EmailAddress] 16 | public string Email { get; set; } 17 | 18 | // treat empty string as null for password fields to 19 | // make them optional in front end apps 20 | private string _password; 21 | [MinLength(6)] 22 | public string Password 23 | { 24 | get => _password; 25 | set => _password = replaceEmptyWithNull(value); 26 | } 27 | 28 | private string _confirmPassword; 29 | [Compare("Password")] 30 | public string ConfirmPassword 31 | { 32 | get => _confirmPassword; 33 | set => _confirmPassword = replaceEmptyWithNull(value); 34 | } 35 | 36 | // helpers 37 | 38 | private string replaceEmptyWithNull(string value) 39 | { 40 | // replace empty string with null to make field optional 41 | return string.IsNullOrEmpty(value) ? null : value; 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Hosting; 3 | 4 | namespace WebApi 5 | { 6 | public class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | CreateHostBuilder(args).Build().Run(); 11 | } 12 | 13 | public static IHostBuilder CreateHostBuilder(string[] args) 14 | { 15 | return Host.CreateDefaultBuilder(args) 16 | .ConfigureWebHostDefaults(x => 17 | { 18 | x.UseStartup(); 19 | x.UseUrls("http://localhost:4000"); 20 | }); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dotnet-5-crud-api 2 | 3 | .NET 5.0 - CRUD API Example 4 | 5 | Documentation and instructions available at https://jasonwatmore.com/post/2021/09/28/net-5-crud-api-example-and-tutorial -------------------------------------------------------------------------------- /Services/UserService.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using BCryptNet = BCrypt.Net.BCrypt; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using WebApi.Entities; 6 | using WebApi.Helpers; 7 | using WebApi.Models.Users; 8 | 9 | namespace WebApi.Services 10 | { 11 | public interface IUserService 12 | { 13 | IEnumerable GetAll(); 14 | User GetById(int id); 15 | void Create(CreateRequest model); 16 | void Update(int id, UpdateRequest model); 17 | void Delete(int id); 18 | } 19 | 20 | public class UserService : IUserService 21 | { 22 | private DataContext _context; 23 | private readonly IMapper _mapper; 24 | 25 | public UserService( 26 | DataContext context, 27 | IMapper mapper) 28 | { 29 | _context = context; 30 | _mapper = mapper; 31 | } 32 | 33 | public IEnumerable GetAll() 34 | { 35 | return _context.Users; 36 | } 37 | 38 | public User GetById(int id) 39 | { 40 | return getUser(id); 41 | } 42 | 43 | public void Create(CreateRequest model) 44 | { 45 | // validate 46 | if (_context.Users.Any(x => x.Email == model.Email)) 47 | throw new AppException("User with the email '" + model.Email + "' already exists"); 48 | 49 | // map model to new user object 50 | var user = _mapper.Map(model); 51 | 52 | // hash password 53 | user.PasswordHash = BCryptNet.HashPassword(model.Password); 54 | 55 | // save user 56 | _context.Users.Add(user); 57 | _context.SaveChanges(); 58 | } 59 | 60 | public void Update(int id, UpdateRequest model) 61 | { 62 | var user = getUser(id); 63 | 64 | // validate 65 | if (model.Email != user.Email && _context.Users.Any(x => x.Email == model.Email)) 66 | throw new AppException("User with the email '" + model.Email + "' already exists"); 67 | 68 | // hash password if it was entered 69 | if (!string.IsNullOrEmpty(model.Password)) 70 | user.PasswordHash = BCryptNet.HashPassword(model.Password); 71 | 72 | // copy model to user and save 73 | _mapper.Map(model, user); 74 | _context.Users.Update(user); 75 | _context.SaveChanges(); 76 | } 77 | 78 | public void Delete(int id) 79 | { 80 | var user = getUser(id); 81 | _context.Users.Remove(user); 82 | _context.SaveChanges(); 83 | } 84 | 85 | // helper methods 86 | 87 | private User getUser(int id) 88 | { 89 | var user = _context.Users.Find(id); 90 | if (user == null) throw new KeyNotFoundException("User not found"); 91 | return user; 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using System; 4 | using System.Text.Json.Serialization; 5 | using WebApi.Helpers; 6 | using WebApi.Services; 7 | 8 | namespace WebApi 9 | { 10 | public class Startup 11 | { 12 | // add services to the DI container 13 | public void ConfigureServices(IServiceCollection services) 14 | { 15 | services.AddDbContext(); 16 | services.AddCors(); 17 | services.AddControllers().AddJsonOptions(x => 18 | { 19 | // serialize enums as strings in api responses (e.g. Role) 20 | x.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); 21 | 22 | // ignore omitted parameters on models to enable optional params (e.g. User update) 23 | x.JsonSerializerOptions.IgnoreNullValues = true; 24 | }); 25 | services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); 26 | 27 | // configure DI for application services 28 | services.AddScoped(); 29 | } 30 | 31 | // configure the HTTP request pipeline 32 | public void Configure(IApplicationBuilder app) 33 | { 34 | app.UseRouting(); 35 | 36 | // global cors policy 37 | app.UseCors(x => x 38 | .AllowAnyOrigin() 39 | .AllowAnyMethod() 40 | .AllowAnyHeader()); 41 | 42 | // global error handler 43 | app.UseMiddleware(); 44 | 45 | app.UseEndpoints(x => x.MapControllers()); 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /WebApi.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net5.0 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Microsoft": "Warning", 5 | "Microsoft.Hosting.Lifetime": "Information" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /omnisharp.json: -------------------------------------------------------------------------------- 1 | { 2 | "msbuild": { 3 | "useBundledOnly": true 4 | } 5 | } --------------------------------------------------------------------------------