├── .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 ├── Repositories └── UserRepository.cs ├── Services └── UserService.cs ├── WebApi.csproj └── appsettings.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 | namespace WebApi.Controllers; 2 | 3 | using Microsoft.AspNetCore.Mvc; 4 | using WebApi.Models.Users; 5 | using WebApi.Services; 6 | 7 | [ApiController] 8 | [Route("[controller]")] 9 | public class UsersController : ControllerBase 10 | { 11 | private IUserService _userService; 12 | 13 | public UsersController(IUserService userService) 14 | { 15 | _userService = userService; 16 | } 17 | 18 | [HttpGet] 19 | public async Task GetAll() 20 | { 21 | var users = await _userService.GetAll(); 22 | return Ok(users); 23 | } 24 | 25 | [HttpGet("{id}")] 26 | public async Task GetById(int id) 27 | { 28 | var user = await _userService.GetById(id); 29 | return Ok(user); 30 | } 31 | 32 | [HttpPost] 33 | public async Task Create(CreateRequest model) 34 | { 35 | await _userService.Create(model); 36 | return Ok(new { message = "User created" }); 37 | } 38 | 39 | [HttpPut("{id}")] 40 | public async Task Update(int id, UpdateRequest model) 41 | { 42 | await _userService.Update(id, model); 43 | return Ok(new { message = "User updated" }); 44 | } 45 | 46 | [HttpDelete("{id}")] 47 | public async Task Delete(int id) 48 | { 49 | await _userService.Delete(id); 50 | return Ok(new { message = "User deleted" }); 51 | } 52 | } -------------------------------------------------------------------------------- /Entities/Role.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.Entities; 2 | 3 | public enum Role 4 | { 5 | Admin, 6 | User 7 | } -------------------------------------------------------------------------------- /Entities/User.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.Entities; 2 | 3 | using System.Text.Json.Serialization; 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 | } -------------------------------------------------------------------------------- /Helpers/AppException.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.Helpers; 2 | 3 | using System.Globalization; 4 | 5 | // custom exception class for throwing application specific exceptions (e.g. for validation) 6 | // that can be caught and handled within the application 7 | public class AppException : Exception 8 | { 9 | public AppException() : base() {} 10 | 11 | public AppException(string message) : base(message) { } 12 | 13 | public AppException(string message, params object[] args) 14 | : base(String.Format(CultureInfo.CurrentCulture, message, args)) 15 | { 16 | } 17 | } -------------------------------------------------------------------------------- /Helpers/AutoMapperProfile.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.Helpers; 2 | 3 | using AutoMapper; 4 | using WebApi.Entities; 5 | using WebApi.Models.Users; 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 | } -------------------------------------------------------------------------------- /Helpers/DataContext.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.Helpers; 2 | 3 | using System.Data; 4 | using Dapper; 5 | using Microsoft.Data.Sqlite; 6 | 7 | public class DataContext 8 | { 9 | protected readonly IConfiguration Configuration; 10 | 11 | public DataContext(IConfiguration configuration) 12 | { 13 | Configuration = configuration; 14 | } 15 | 16 | public IDbConnection CreateConnection() 17 | { 18 | return new SqliteConnection(Configuration.GetConnectionString("WebApiDatabase")); 19 | } 20 | 21 | public async Task Init() 22 | { 23 | // create database tables if they don't exist 24 | using var connection = CreateConnection(); 25 | await _initUsers(); 26 | 27 | async Task _initUsers() 28 | { 29 | var sql = """ 30 | CREATE TABLE IF NOT EXISTS 31 | Users ( 32 | Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 33 | Title TEXT, 34 | FirstName TEXT, 35 | LastName TEXT, 36 | Email TEXT, 37 | Role INTEGER, 38 | PasswordHash TEXT 39 | ); 40 | """; 41 | await connection.ExecuteAsync(sql); 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /Helpers/ErrorHandlerMiddleware.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.Helpers; 2 | 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.Extensions.Logging; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Net; 8 | using System.Text.Json; 9 | using System.Threading.Tasks; 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 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 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 | namespace WebApi.Models.Users; 2 | 3 | using System.ComponentModel.DataAnnotations; 4 | using WebApi.Entities; 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 | } -------------------------------------------------------------------------------- /Models/Users/UpdateRequest.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.Models.Users; 2 | 3 | using System.ComponentModel.DataAnnotations; 4 | using WebApi.Entities; 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 | } -------------------------------------------------------------------------------- /Program.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using WebApi.Helpers; 3 | using WebApi.Repositories; 4 | using WebApi.Services; 5 | 6 | var builder = WebApplication.CreateBuilder(args); 7 | 8 | // add services to DI container 9 | { 10 | var services = builder.Services; 11 | var env = builder.Environment; 12 | 13 | services.AddSingleton(); 14 | services.AddCors(); 15 | services.AddControllers().AddJsonOptions(x => 16 | { 17 | // serialize enums as strings in api responses (e.g. Role) 18 | x.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); 19 | 20 | // ignore omitted parameters on models to enable optional params (e.g. User update) 21 | x.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; 22 | }); 23 | services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); 24 | 25 | // configure DI for application services 26 | services.AddScoped(); 27 | services.AddScoped(); 28 | } 29 | 30 | var app = builder.Build(); 31 | 32 | // ensure database and tables exist 33 | { 34 | using var scope = app.Services.CreateScope(); 35 | var context = scope.ServiceProvider.GetRequiredService(); 36 | await context.Init(); 37 | } 38 | 39 | // configure HTTP request pipeline 40 | { 41 | // global cors policy 42 | app.UseCors(x => x 43 | .AllowAnyOrigin() 44 | .AllowAnyMethod() 45 | .AllowAnyHeader()); 46 | 47 | // global error handler 48 | app.UseMiddleware(); 49 | 50 | app.MapControllers(); 51 | } 52 | 53 | app.Run("http://localhost:4000"); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dotnet-7-dapper-sqlite-crud-api 2 | 3 | .NET 7.0 + Dapper + SQLite - CRUD API Tutorial in ASP.NET Core 4 | 5 | Documentation at https://jasonwatmore.com/net-7-dapper-sqlite-crud-api-tutorial-in-aspnet-core -------------------------------------------------------------------------------- /Repositories/UserRepository.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.Repositories; 2 | 3 | using Dapper; 4 | using WebApi.Entities; 5 | using WebApi.Helpers; 6 | 7 | public interface IUserRepository 8 | { 9 | Task> GetAll(); 10 | Task GetById(int id); 11 | Task GetByEmail(string email); 12 | Task Create(User user); 13 | Task Update(User user); 14 | Task Delete(int id); 15 | } 16 | 17 | public class UserRepository : IUserRepository 18 | { 19 | private DataContext _context; 20 | 21 | public UserRepository(DataContext context) 22 | { 23 | _context = context; 24 | } 25 | 26 | public async Task> GetAll() 27 | { 28 | using var connection = _context.CreateConnection(); 29 | var sql = """ 30 | SELECT * FROM Users 31 | """; 32 | return await connection.QueryAsync(sql); 33 | } 34 | 35 | public async Task GetById(int id) 36 | { 37 | using var connection = _context.CreateConnection(); 38 | var sql = """ 39 | SELECT * FROM Users 40 | WHERE Id = @id 41 | """; 42 | return await connection.QuerySingleOrDefaultAsync(sql, new { id }); 43 | } 44 | 45 | public async Task GetByEmail(string email) 46 | { 47 | using var connection = _context.CreateConnection(); 48 | var sql = """ 49 | SELECT * FROM Users 50 | WHERE Email = @email 51 | """; 52 | return await connection.QuerySingleOrDefaultAsync(sql, new { email }); 53 | } 54 | 55 | public async Task Create(User user) 56 | { 57 | using var connection = _context.CreateConnection(); 58 | var sql = """ 59 | INSERT INTO Users (Title, FirstName, LastName, Email, Role, PasswordHash) 60 | VALUES (@Title, @FirstName, @LastName, @Email, @Role, @PasswordHash) 61 | """; 62 | await connection.ExecuteAsync(sql, user); 63 | } 64 | 65 | public async Task Update(User user) 66 | { 67 | using var connection = _context.CreateConnection(); 68 | var sql = """ 69 | UPDATE Users 70 | SET Title = @Title, 71 | FirstName = @FirstName, 72 | LastName = @LastName, 73 | Email = @Email, 74 | Role = @Role, 75 | PasswordHash = @PasswordHash 76 | WHERE Id = @Id 77 | """; 78 | await connection.ExecuteAsync(sql, user); 79 | } 80 | 81 | public async Task Delete(int id) 82 | { 83 | using var connection = _context.CreateConnection(); 84 | var sql = """ 85 | DELETE FROM Users 86 | WHERE Id = @id 87 | """; 88 | await connection.ExecuteAsync(sql, new { id }); 89 | } 90 | } -------------------------------------------------------------------------------- /Services/UserService.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.Services; 2 | 3 | using AutoMapper; 4 | using BCrypt.Net; 5 | using WebApi.Entities; 6 | using WebApi.Helpers; 7 | using WebApi.Models.Users; 8 | using WebApi.Repositories; 9 | 10 | public interface IUserService 11 | { 12 | Task> GetAll(); 13 | Task GetById(int id); 14 | Task Create(CreateRequest model); 15 | Task Update(int id, UpdateRequest model); 16 | Task Delete(int id); 17 | } 18 | 19 | public class UserService : IUserService 20 | { 21 | private IUserRepository _userRepository; 22 | private readonly IMapper _mapper; 23 | 24 | public UserService( 25 | IUserRepository userRepository, 26 | IMapper mapper) 27 | { 28 | _userRepository = userRepository; 29 | _mapper = mapper; 30 | } 31 | 32 | public async Task> GetAll() 33 | { 34 | return await _userRepository.GetAll(); 35 | } 36 | 37 | public async Task GetById(int id) 38 | { 39 | var user = await _userRepository.GetById(id); 40 | 41 | if (user == null) 42 | throw new KeyNotFoundException("User not found"); 43 | 44 | return user; 45 | } 46 | 47 | public async Task Create(CreateRequest model) 48 | { 49 | // validate 50 | if (await _userRepository.GetByEmail(model.Email!) != null) 51 | throw new AppException("User with the email '" + model.Email + "' already exists"); 52 | 53 | // map model to new user object 54 | var user = _mapper.Map(model); 55 | 56 | // hash password 57 | user.PasswordHash = BCrypt.HashPassword(model.Password); 58 | 59 | // save user 60 | await _userRepository.Create(user); 61 | } 62 | 63 | public async Task Update(int id, UpdateRequest model) 64 | { 65 | var user = await _userRepository.GetById(id); 66 | 67 | if (user == null) 68 | throw new KeyNotFoundException("User not found"); 69 | 70 | // validate 71 | var emailChanged = !string.IsNullOrEmpty(model.Email) && user.Email != model.Email; 72 | if (emailChanged && await _userRepository.GetByEmail(model.Email!) != null) 73 | throw new AppException("User with the email '" + model.Email + "' already exists"); 74 | 75 | // hash password if it was entered 76 | if (!string.IsNullOrEmpty(model.Password)) 77 | user.PasswordHash = BCrypt.HashPassword(model.Password); 78 | 79 | // copy model props to user 80 | _mapper.Map(model, user); 81 | 82 | // save user 83 | await _userRepository.Update(user); 84 | } 85 | 86 | public async Task Delete(int id) 87 | { 88 | await _userRepository.Delete(id); 89 | } 90 | } -------------------------------------------------------------------------------- /WebApi.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net7.0 4 | enable 5 | enable 6 | WebApi 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "WebApiDatabase": "Data Source=LocalDatabase.db" 4 | }, 5 | "Logging": { 6 | "LogLevel": { 7 | "Default": "Information", 8 | "Microsoft.AspNetCore": "Warning" 9 | } 10 | } 11 | } --------------------------------------------------------------------------------