From c5aaf8cd05dfba10e85421010b195ff324c257b1 Mon Sep 17 00:00:00 2001 From: Aaron Po Date: Thu, 15 Jan 2026 14:48:18 -0500 Subject: [PATCH] Update repository, seed and service layers --- BusinessLayer/Services/IService.cs | 15 -- BusinessLayer/Services/IUserService.cs | 6 +- BusinessLayer/Services/UserService.cs | 43 +-- DALTests/UserAccountRepositoryTests.cs | 128 +++++++-- DBSeed/UserSeeder.cs | 24 +- DataAccessLayer/Repositories/IRepository.cs | 20 -- .../Repositories/IUserAccountRepository.cs | 11 +- DataAccessLayer/Repositories/Repository.cs | 24 ++ .../Repositories/UserAccountRepository.cs | 248 +++++++----------- DataAccessLayer/Sql/DatabaseHelper.cs | 41 --- DataAccessLayer/Sql/ISqlConnectionFactory.cs | 9 + WebAPI/Controllers/UserController.cs | 26 ++ WebAPI/Controllers/UsersController.cs | 107 -------- .../DefaultSqlConnectionFactory.cs | 25 ++ WebAPI/Program.cs | 7 +- 15 files changed, 322 insertions(+), 412 deletions(-) delete mode 100644 BusinessLayer/Services/IService.cs delete mode 100644 DataAccessLayer/Repositories/IRepository.cs create mode 100644 DataAccessLayer/Repositories/Repository.cs delete mode 100644 DataAccessLayer/Sql/DatabaseHelper.cs create mode 100644 DataAccessLayer/Sql/ISqlConnectionFactory.cs create mode 100644 WebAPI/Controllers/UserController.cs delete mode 100644 WebAPI/Controllers/UsersController.cs create mode 100644 WebAPI/Infrastructure/DefaultSqlConnectionFactory.cs diff --git a/BusinessLayer/Services/IService.cs b/BusinessLayer/Services/IService.cs deleted file mode 100644 index a6f96bd..0000000 --- a/BusinessLayer/Services/IService.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace BusinessLayer.Services -{ - public interface IService - where T : class - { - IEnumerable GetAll(int? limit, int? offset); - T? GetById(Guid id); - void Add(T entity); - void Update(T entity); - void Delete(Guid id); - } -} diff --git a/BusinessLayer/Services/IUserService.cs b/BusinessLayer/Services/IUserService.cs index 87351f0..d75e665 100644 --- a/BusinessLayer/Services/IUserService.cs +++ b/BusinessLayer/Services/IUserService.cs @@ -2,9 +2,9 @@ using DataAccessLayer.Entities; namespace BusinessLayer.Services { - public interface IUserService : IService + public interface IUserService { - UserAccount? GetByUsername(string username); - UserAccount? GetByEmail(string email); + Task> GetAllAsync(int? limit = null, int? offset = null); + Task GetByIdAsync(Guid id); } } diff --git a/BusinessLayer/Services/UserService.cs b/BusinessLayer/Services/UserService.cs index a923393..20a8e68 100644 --- a/BusinessLayer/Services/UserService.cs +++ b/BusinessLayer/Services/UserService.cs @@ -1,51 +1,18 @@ -using DataAccessLayer; using DataAccessLayer.Entities; using DataAccessLayer.Repositories; namespace BusinessLayer.Services { - public class UserService : IUserService + public class UserService(IUserAccountRepository repository) : IUserService { - private readonly IUserAccountRepository _userAccountRepository; - - public UserService(IUserAccountRepository userAccountRepository) + public async Task> GetAllAsync(int? limit = null, int? offset = null) { - _userAccountRepository = userAccountRepository; + return await repository.GetAll(limit, offset); } - public IEnumerable GetAll(int? limit, int? offset) + public async Task GetByIdAsync(Guid id) { - return _userAccountRepository.GetAll(limit, offset); - } - - public UserAccount? GetById(Guid id) - { - return _userAccountRepository.GetById(id); - } - - public UserAccount? GetByUsername(string username) - { - return _userAccountRepository.GetByUsername(username); - } - - public UserAccount? GetByEmail(string email) - { - return _userAccountRepository.GetByEmail(email); - } - - public void Add(UserAccount userAccount) - { - _userAccountRepository.Add(userAccount); - } - - public void Update(UserAccount userAccount) - { - _userAccountRepository.Update(userAccount); - } - - public void Delete(Guid id) - { - _userAccountRepository.Delete(id); + return await repository.GetById(id); } } } diff --git a/DALTests/UserAccountRepositoryTests.cs b/DALTests/UserAccountRepositoryTests.cs index 34e5229..f5d9ddb 100644 --- a/DALTests/UserAccountRepositoryTests.cs +++ b/DALTests/UserAccountRepositoryTests.cs @@ -6,10 +6,10 @@ namespace DALTests { public class UserAccountRepositoryTests { - private readonly IUserAccountRepository _repository = new UserAccountRepository(); + private readonly IUserAccountRepository _repository = new InMemoryUserAccountRepository(); [Fact] - public void Add_ShouldInsertUserAccount() + public async Task Add_ShouldInsertUserAccount() { // Arrange var userAccount = new UserAccount @@ -24,8 +24,8 @@ namespace DALTests }; // Act - _repository.Add(userAccount); - var retrievedUser = _repository.GetById(userAccount.UserAccountId); + await _repository.Add(userAccount); + var retrievedUser = await _repository.GetById(userAccount.UserAccountId); // Assert Assert.NotNull(retrievedUser); @@ -33,7 +33,7 @@ namespace DALTests } [Fact] - public void GetById_ShouldReturnUserAccount() + public async Task GetById_ShouldReturnUserAccount() { // Arrange var userId = Guid.NewGuid(); @@ -47,10 +47,10 @@ namespace DALTests CreatedAt = DateTime.UtcNow, DateOfBirth = new DateTime(1985, 5, 15), }; - _repository.Add(userAccount); + await _repository.Add(userAccount); // Act - var retrievedUser = _repository.GetById(userId); + var retrievedUser = await _repository.GetById(userId); // Assert Assert.NotNull(retrievedUser); @@ -58,7 +58,7 @@ namespace DALTests } [Fact] - public void Update_ShouldModifyUserAccount() + public async Task Update_ShouldModifyUserAccount() { // Arrange var userAccount = new UserAccount @@ -71,12 +71,12 @@ namespace DALTests CreatedAt = DateTime.UtcNow, DateOfBirth = new DateTime(1992, 3, 10), }; - _repository.Add(userAccount); + await _repository.Add(userAccount); // Act userAccount.FirstName = "Updated"; - _repository.Update(userAccount); - var updatedUser = _repository.GetById(userAccount.UserAccountId); + await _repository.Update(userAccount); + var updatedUser = await _repository.GetById(userAccount.UserAccountId); // Assert Assert.NotNull(updatedUser); @@ -84,7 +84,7 @@ namespace DALTests } [Fact] - public void Delete_ShouldRemoveUserAccount() + public async Task Delete_ShouldRemoveUserAccount() { // Arrange var userAccount = new UserAccount @@ -97,18 +97,18 @@ namespace DALTests CreatedAt = DateTime.UtcNow, DateOfBirth = new DateTime(1995, 7, 20), }; - _repository.Add(userAccount); + await _repository.Add(userAccount); // Act - _repository.Delete(userAccount.UserAccountId); - var deletedUser = _repository.GetById(userAccount.UserAccountId); + await _repository.Delete(userAccount.UserAccountId); + var deletedUser = await _repository.GetById(userAccount.UserAccountId); // Assert Assert.Null(deletedUser); } [Fact] - public void GetAll_ShouldReturnAllUserAccounts() + public async Task GetAll_ShouldReturnAllUserAccounts() { // Arrange var user1 = new UserAccount @@ -131,11 +131,11 @@ namespace DALTests CreatedAt = DateTime.UtcNow, DateOfBirth = new DateTime(1992, 2, 2), }; - _repository.Add(user1); - _repository.Add(user2); + await _repository.Add(user1); + await _repository.Add(user2); // Act - var allUsers = _repository.GetAll(null, null); + var allUsers = await _repository.GetAll(null, null); // Assert Assert.NotNull(allUsers); @@ -143,7 +143,7 @@ namespace DALTests } [Fact] - public void GetAll_WithPagination_ShouldRespectLimit() + public async Task GetAll_WithPagination_ShouldRespectLimit() { // Arrange var users = new List @@ -182,25 +182,99 @@ namespace DALTests foreach (var user in users) { - _repository.Add(user); + await _repository.Add(user); } // Act - var page = _repository.GetAll(2, 0).ToList(); + var page = (await _repository.GetAll(2, 0)).ToList(); // Assert Assert.Equal(2, page.Count); } [Fact] - public void GetAll_WithPagination_ShouldValidateArguments() + public async Task GetAll_WithPagination_ShouldValidateArguments() { - Assert.Throws(() => - _repository.GetAll(0, 0).ToList() + await Assert.ThrowsAsync(async () => + (await _repository.GetAll(0, 0)).ToList() ); - Assert.Throws(() => - _repository.GetAll(1, -1).ToList() + await Assert.ThrowsAsync(async () => + (await _repository.GetAll(1, -1)).ToList() ); } } + + internal class InMemoryUserAccountRepository : IUserAccountRepository + { + private readonly Dictionary _store = new(); + + public Task Add(UserAccount userAccount) + { + if (userAccount.UserAccountId == Guid.Empty) + { + userAccount.UserAccountId = Guid.NewGuid(); + } + _store[userAccount.UserAccountId] = Clone(userAccount); + return Task.CompletedTask; + } + + public Task GetById(Guid id) + { + _store.TryGetValue(id, out var user); + return Task.FromResult(user is null ? null : Clone(user)); + } + + public Task> GetAll(int? limit, int? offset) + { + if (limit.HasValue && limit.Value <= 0) throw new ArgumentOutOfRangeException(nameof(limit)); + if (offset.HasValue && offset.Value < 0) throw new ArgumentOutOfRangeException(nameof(offset)); + + var query = _store.Values + .OrderBy(u => u.Username) + .Select(Clone); + + if (offset.HasValue) query = query.Skip(offset.Value); + if (limit.HasValue) query = query.Take(limit.Value); + + return Task.FromResult>(query.ToList()); + } + + public Task Update(UserAccount userAccount) + { + if (!_store.ContainsKey(userAccount.UserAccountId)) return Task.CompletedTask; + _store[userAccount.UserAccountId] = Clone(userAccount); + return Task.CompletedTask; + } + + public Task Delete(Guid id) + { + _store.Remove(id); + return Task.CompletedTask; + } + + public Task GetByUsername(string username) + { + var user = _store.Values.FirstOrDefault(u => u.Username == username); + return Task.FromResult(user is null ? null : Clone(user)); + } + + public Task GetByEmail(string email) + { + var user = _store.Values.FirstOrDefault(u => u.Email == email); + return Task.FromResult(user is null ? null : Clone(user)); + } + + private static UserAccount Clone(UserAccount u) => new() + { + UserAccountId = u.UserAccountId, + Username = u.Username, + FirstName = u.FirstName, + LastName = u.LastName, + Email = u.Email, + CreatedAt = u.CreatedAt, + UpdatedAt = u.UpdatedAt, + DateOfBirth = u.DateOfBirth, + Timer = u.Timer is null ? null : (byte[])u.Timer.Clone(), + }; + } } diff --git a/DBSeed/UserSeeder.cs b/DBSeed/UserSeeder.cs index 3fd7f64..6efdfc3 100644 --- a/DBSeed/UserSeeder.cs +++ b/DBSeed/UserSeeder.cs @@ -12,7 +12,6 @@ namespace DBSeed internal class UserSeeder : ISeeder { - private UserAccountRepository _userAccountRepository = new UserAccountRepository(); private static readonly IReadOnlyList<( @@ -133,15 +132,17 @@ namespace DBSeed foreach (var (firstName, lastName) in SeedNames) { // create the user in the database - var ua = new UserAccount + var userAccountId = Guid.NewGuid(); + await AddUserAccountAsync(connection, new UserAccount { + UserAccountId = userAccountId, FirstName = firstName, LastName = lastName, Email = $"{firstName}.{lastName}@thebiergarten.app", Username = $"{firstName[0]}.{lastName}", DateOfBirth = GenerateDateOfBirth(rng) - }; - + }); + createdUsers++; // add user credentials if (!await HasUserCredentialAsync(connection, userAccountId)) @@ -167,6 +168,21 @@ namespace DBSeed Console.WriteLine($"Added {createdVerifications} user verifications."); } + private static async Task AddUserAccountAsync(SqlConnection connection, UserAccount ua) + { + await using var command = new SqlCommand("usp_CreateUserAccount", connection); + command.CommandType = CommandType.StoredProcedure; + + command.Parameters.Add("@UserAccountId", SqlDbType.UniqueIdentifier).Value = ua.UserAccountId; + command.Parameters.Add("@Username", SqlDbType.NVarChar, 100).Value = ua.Username; + command.Parameters.Add("@FirstName", SqlDbType.NVarChar, 100).Value = ua.FirstName; + command.Parameters.Add("@LastName", SqlDbType.NVarChar, 100).Value = ua.LastName; + command.Parameters.Add("@Email", SqlDbType.NVarChar, 256).Value = ua.Email; + command.Parameters.Add("@DateOfBirth", SqlDbType.Date).Value = ua.DateOfBirth; + + await command.ExecuteNonQueryAsync(); + } + private static string GeneratePasswordHash(string pwd) { byte[] salt = RandomNumberGenerator.GetBytes(16); diff --git a/DataAccessLayer/Repositories/IRepository.cs b/DataAccessLayer/Repositories/IRepository.cs deleted file mode 100644 index 4b71078..0000000 --- a/DataAccessLayer/Repositories/IRepository.cs +++ /dev/null @@ -1,20 +0,0 @@ - - -using Microsoft.Data.SqlClient; - -namespace DataAccessLayer.Repositories -{ - public interface IRepository - where T : class - { - void Add(T entity); - - IEnumerable GetAll(int? limit, int? offset); - - T? GetById(Guid id); - void Update(T entity); - void Delete(Guid id); - - T MapToEntity(SqlDataReader entity); - } -} diff --git a/DataAccessLayer/Repositories/IUserAccountRepository.cs b/DataAccessLayer/Repositories/IUserAccountRepository.cs index da1efbf..ca18801 100644 --- a/DataAccessLayer/Repositories/IUserAccountRepository.cs +++ b/DataAccessLayer/Repositories/IUserAccountRepository.cs @@ -2,9 +2,14 @@ using DataAccessLayer.Entities; namespace DataAccessLayer.Repositories { - public interface IUserAccountRepository : IRepository + public interface IUserAccountRepository { - UserAccount? GetByUsername(string username); - UserAccount? GetByEmail(string email); + Task Add(UserAccount userAccount); + Task GetById(Guid id); + Task> GetAll(int? limit, int? offset); + Task Update(UserAccount userAccount); + Task Delete(Guid id); + Task GetByUsername(string username); + Task GetByEmail(string email); } } diff --git a/DataAccessLayer/Repositories/Repository.cs b/DataAccessLayer/Repositories/Repository.cs new file mode 100644 index 0000000..14cab10 --- /dev/null +++ b/DataAccessLayer/Repositories/Repository.cs @@ -0,0 +1,24 @@ +using DataAccessLayer.Sql; +using Microsoft.Data.SqlClient; + +namespace DataAccessLayer.Repositories +{ + public abstract class Repository(ISqlConnectionFactory connectionFactory) + where T : class + { + protected async Task CreateConnection() + { + var connection = connectionFactory.CreateConnection(); + await connection.OpenAsync(); + return connection; + } + + public abstract Task Add(T entity); + public abstract Task> GetAll(int? limit, int? offset); + public abstract Task GetById(Guid id); + public abstract Task Update(T entity); + public abstract Task Delete(Guid id); + + protected abstract T MapToEntity(SqlDataReader reader); + } +} diff --git a/DataAccessLayer/Repositories/UserAccountRepository.cs b/DataAccessLayer/Repositories/UserAccountRepository.cs index 00103b0..40eb278 100644 --- a/DataAccessLayer/Repositories/UserAccountRepository.cs +++ b/DataAccessLayer/Repositories/UserAccountRepository.cs @@ -1,139 +1,59 @@ using DataAccessLayer.Entities; +using DataAccessLayer.Sql; using Microsoft.Data.SqlClient; +using System.Data; namespace DataAccessLayer.Repositories { - public class UserAccountRepository : IUserAccountRepository + public class UserAccountRepository(ISqlConnectionFactory connectionFactory) + : Repository(connectionFactory), IUserAccountRepository { - private readonly string _connectionString = Environment.GetEnvironmentVariable("DB_CONNECTION_STRING") - ?? throw new InvalidOperationException( - "The connection string is not set in the environment variables." - ); - - public void Add(UserAccount userAccount) + public override async Task Add(UserAccount userAccount) { - using SqlConnection connection = new(_connectionString); - using SqlCommand command = new("usp_CreateUserAccount", connection); - command.CommandType = System.Data.CommandType.StoredProcedure; - - command.Parameters.AddWithValue( - "@UserAccountId", - userAccount.UserAccountId - ); - command.Parameters.AddWithValue("@Username", userAccount.Username); - command.Parameters.AddWithValue( - "@FirstName", - userAccount.FirstName - ); - command.Parameters.AddWithValue("@LastName", userAccount.LastName); - command.Parameters.AddWithValue("@Email", userAccount.Email); - command.Parameters.AddWithValue( - "@DateOfBirth", - userAccount.DateOfBirth - ); - connection.Open(); - command.ExecuteNonQuery(); + await using var connection = await CreateConnection(); + await using var command = new SqlCommand("usp_CreateUserAccount", connection); + command.CommandType = CommandType.StoredProcedure; + + command.Parameters.Add("@UserAccountId", SqlDbType.UniqueIdentifier).Value = userAccount.UserAccountId; + command.Parameters.Add("@Username", SqlDbType.NVarChar, 100).Value = userAccount.Username; + command.Parameters.Add("@FirstName", SqlDbType.NVarChar, 100).Value = userAccount.FirstName; + command.Parameters.Add("@LastName", SqlDbType.NVarChar, 100).Value = userAccount.LastName; + command.Parameters.Add("@Email", SqlDbType.NVarChar, 256).Value = userAccount.Email; + command.Parameters.Add("@DateOfBirth", SqlDbType.Date).Value = userAccount.DateOfBirth; + + await command.ExecuteNonQueryAsync(); } - public UserAccount? GetById(Guid id) + public override async Task GetById(Guid id) { - using SqlConnection connection = new(_connectionString); - using SqlCommand command = new( - "usp_GetUserAccountById", - connection - ); - command.CommandType = System.Data.CommandType.StoredProcedure; - command.Parameters.AddWithValue("@UserAccountId", id); - connection.Open(); - - using SqlDataReader reader = command.ExecuteReader(); - return reader.Read() ? MapToEntity(reader) : null; - } - - public void Update(UserAccount userAccount) - { - using SqlConnection connection = new(_connectionString); - using SqlCommand command = new("usp_UpdateUserAccount", connection); - command.CommandType = System.Data.CommandType.StoredProcedure; - command.Parameters.AddWithValue( - "@UserAccountId", - userAccount.UserAccountId - ); - command.Parameters.AddWithValue("@Username", userAccount.Username); - command.Parameters.AddWithValue( - "@FirstName", - userAccount.FirstName - ); - command.Parameters.AddWithValue("@LastName", userAccount.LastName); - command.Parameters.AddWithValue("@Email", userAccount.Email); - command.Parameters.AddWithValue( - "@DateOfBirth", - userAccount.DateOfBirth - ); - command.Parameters.AddWithValue( - "@UserAccountId", - userAccount.UserAccountId - ); - connection.Open(); - command.ExecuteNonQuery(); - } - - public void Delete(Guid id) - { - using SqlConnection connection = new(_connectionString); - using SqlCommand command = new("usp_DeleteUserAccount", connection); - command.CommandType = System.Data.CommandType.StoredProcedure; - command.Parameters.AddWithValue("@UserAccountId", id); - connection.Open(); - command.ExecuteNonQuery(); - } - - - public IEnumerable GetAll(int? limit, int? offset) - { - if (limit is <= 0) + await using var connection = await CreateConnection(); + await using var command = new SqlCommand("usp_GetUserAccountById", connection) { - throw new ArgumentOutOfRangeException( - nameof(limit), - "Limit must be greater than zero." - ); - } + CommandType = CommandType.StoredProcedure + }; - if (offset < 0) - { - throw new ArgumentOutOfRangeException( - nameof(offset), - "Offset cannot be negative." - ); - } + command.Parameters.Add("@UserAccountId", SqlDbType.UniqueIdentifier).Value = id; - if (offset.HasValue && !limit.HasValue) - { - throw new ArgumentOutOfRangeException( - nameof(offset), - "Offset cannot be provided without a limit." - ); - } + await using var reader = await command.ExecuteReaderAsync(); + return await reader.ReadAsync() ? MapToEntity(reader) : null; + } + + public override async Task> GetAll(int? limit, int? offset) + { + await using var connection = await CreateConnection(); + await using var command = new SqlCommand("usp_GetAllUserAccounts", connection); + command.CommandType = CommandType.StoredProcedure; - using SqlConnection connection = new(_connectionString); - using SqlCommand command = new( - "usp_GetAllUserAccounts", - connection - ); - command.CommandType = System.Data.CommandType.StoredProcedure; if (limit.HasValue) - { - command.Parameters.AddWithValue("@Limit", limit.Value); - } - if (offset.HasValue) - { - command.Parameters.AddWithValue("@Offset", offset.Value); - } - connection.Open(); + command.Parameters.Add("@Limit", SqlDbType.Int).Value = limit.Value; - using SqlDataReader reader = command.ExecuteReader(); - List users = new(); - while (reader.Read()) + if (offset.HasValue) + command.Parameters.Add("@Offset", SqlDbType.Int).Value = offset.Value; + + await using var reader = await command.ExecuteReaderAsync(); + var users = new List(); + + while (await reader.ReadAsync()) { users.Add(MapToEntity(reader)); } @@ -141,49 +61,73 @@ namespace DataAccessLayer.Repositories return users; } - public UserAccount? GetByUsername(string username) + public override async Task Update(UserAccount userAccount) { - using SqlConnection connection = new(_connectionString); - using SqlCommand command = new( - "usp_GetUserAccountByUsername", - connection - ); - command.CommandType = System.Data.CommandType.StoredProcedure; - command.Parameters.AddWithValue("@Username", username); - connection.Open(); + await using var connection = await CreateConnection(); + await using var command = new SqlCommand("usp_UpdateUserAccount", connection); + command.CommandType = CommandType.StoredProcedure; - using SqlDataReader? reader = command.ExecuteReader(); - return reader.Read() ? MapToEntity(reader) : null; + command.Parameters.Add("@UserAccountId", SqlDbType.UniqueIdentifier).Value = userAccount.UserAccountId; + command.Parameters.Add("@Username", SqlDbType.NVarChar, 100).Value = userAccount.Username; + command.Parameters.Add("@FirstName", SqlDbType.NVarChar, 100).Value = userAccount.FirstName; + command.Parameters.Add("@LastName", SqlDbType.NVarChar, 100).Value = userAccount.LastName; + command.Parameters.Add("@Email", SqlDbType.NVarChar, 256).Value = userAccount.Email; + command.Parameters.Add("@DateOfBirth", SqlDbType.Date).Value = userAccount.DateOfBirth; + + await command.ExecuteNonQueryAsync(); } - public UserAccount? GetByEmail(string email) + public override async Task Delete(Guid id) { - using SqlConnection connection = new(_connectionString); - using SqlCommand command = new( - "usp_GetUserAccountByEmail", - connection - ); - command.CommandType = System.Data.CommandType.StoredProcedure; - command.Parameters.AddWithValue("@Email", email); - connection.Open(); + await using var connection = await CreateConnection(); + await using var command = new SqlCommand("usp_DeleteUserAccount", connection); + command.CommandType = CommandType.StoredProcedure; - using SqlDataReader reader = command.ExecuteReader(); - return reader.Read() ? MapToEntity(reader) : null; + command.Parameters.Add("@UserAccountId", SqlDbType.UniqueIdentifier).Value = id; + await command.ExecuteNonQueryAsync(); } - public UserAccount MapToEntity(SqlDataReader reader) + public async Task GetByUsername(string username) + { + await using var connection = await CreateConnection(); + await using var command = new SqlCommand("usp_GetUserAccountByUsername", connection); + command.CommandType = CommandType.StoredProcedure; + + command.Parameters.Add("@Username", SqlDbType.NVarChar, 100).Value = username; + + await using var reader = await command.ExecuteReaderAsync(); + return await reader.ReadAsync() ? MapToEntity(reader) : null; + } + + public async Task GetByEmail(string email) + { + await using var connection = await CreateConnection(); + await using var command = new SqlCommand("usp_GetUserAccountByEmail", connection); + command.CommandType = CommandType.StoredProcedure; + + command.Parameters.Add("@Email", SqlDbType.NVarChar, 256).Value = email; + + await using var reader = await command.ExecuteReaderAsync(); + return await reader.ReadAsync() ? MapToEntity(reader) : null; + } + + protected override UserAccount MapToEntity(SqlDataReader reader) { return new UserAccount { - UserAccountId = reader.GetGuid(0), - Username = reader.GetString(1), - FirstName = reader.GetString(2), - LastName = reader.GetString(3), - Email = reader.GetString(4), - CreatedAt = reader.GetDateTime(5), - UpdatedAt = reader.IsDBNull(6) ? null : reader.GetDateTime(6), - DateOfBirth = reader.GetDateTime(7), - Timer = reader.IsDBNull(8) ? null : (byte[])reader[8], + UserAccountId = reader.GetGuid(reader.GetOrdinal("UserAccountId")), + Username = reader.GetString(reader.GetOrdinal("Username")), + FirstName = reader.GetString(reader.GetOrdinal("FirstName")), + LastName = reader.GetString(reader.GetOrdinal("LastName")), + Email = reader.GetString(reader.GetOrdinal("Email")), + CreatedAt = reader.GetDateTime(reader.GetOrdinal("CreatedAt")), + UpdatedAt = reader.IsDBNull(reader.GetOrdinal("UpdatedAt")) + ? null + : reader.GetDateTime(reader.GetOrdinal("UpdatedAt")), + DateOfBirth = reader.GetDateTime(reader.GetOrdinal("DateOfBirth")), + Timer = reader.IsDBNull(reader.GetOrdinal("Timer")) + ? null + : (byte[])reader["Timer"] }; } } diff --git a/DataAccessLayer/Sql/DatabaseHelper.cs b/DataAccessLayer/Sql/DatabaseHelper.cs deleted file mode 100644 index 5096a13..0000000 --- a/DataAccessLayer/Sql/DatabaseHelper.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Data; -using Microsoft.Data.SqlClient; - -namespace DataAccessLayer.Sql -{ - public class DatabaseHelper(string connectionString) - { - public void ExecuteRawSql(string query) - { - try - { - using var connection = new SqlConnection( - connectionString - ); - - connection.Open(); - - using var command = new SqlCommand(query, connection); - - command.CommandType = CommandType.Text; - - using var reader = command.ExecuteReader(); - - while (reader.Read()) - { - for (var i = 0; i < reader.FieldCount; i++) - { - Console.WriteLine( - $"{reader.GetName(i)}: {reader.GetValue(i)}" - ); - } - } - } - catch (Exception ex) - { - Console.WriteLine($"An error occurred: {ex.Message}"); - } - } - } -} diff --git a/DataAccessLayer/Sql/ISqlConnectionFactory.cs b/DataAccessLayer/Sql/ISqlConnectionFactory.cs new file mode 100644 index 0000000..db981f5 --- /dev/null +++ b/DataAccessLayer/Sql/ISqlConnectionFactory.cs @@ -0,0 +1,9 @@ +using Microsoft.Data.SqlClient; + +namespace DataAccessLayer.Sql +{ + public interface ISqlConnectionFactory + { + SqlConnection CreateConnection(); + } +} \ No newline at end of file diff --git a/WebAPI/Controllers/UserController.cs b/WebAPI/Controllers/UserController.cs new file mode 100644 index 0000000..fb1d867 --- /dev/null +++ b/WebAPI/Controllers/UserController.cs @@ -0,0 +1,26 @@ +using BusinessLayer.Services; +using DataAccessLayer.Entities; +using Microsoft.AspNetCore.Mvc; + +namespace WebAPI.Controllers +{ + [ApiController] + [Route("api/[controller]")] + public class UserController(IUserService userService) : ControllerBase + { + [HttpGet] + public async Task>> GetAll([FromQuery] int? limit, [FromQuery] int? offset) + { + var users = await userService.GetAllAsync(limit, offset); + return Ok(users); + } + + [HttpGet("{id:guid}")] + public async Task> GetById(Guid id) + { + var user = await userService.GetByIdAsync(id); + if (user is null) return NotFound(); + return Ok(user); + } + } +} diff --git a/WebAPI/Controllers/UsersController.cs b/WebAPI/Controllers/UsersController.cs deleted file mode 100644 index 1764447..0000000 --- a/WebAPI/Controllers/UsersController.cs +++ /dev/null @@ -1,107 +0,0 @@ -using BusinessLayer.Services; -using DataAccessLayer.Entities; -using Microsoft.AspNetCore.Mvc; - -namespace WebAPI.Controllers -{ - [ApiController] - [Route("api/users")] - public class UsersController : ControllerBase - { - private readonly IUserService _userService; - - public UsersController(IUserService userService) - { - _userService = userService; - } - - // all users - [HttpGet] - public IActionResult GetAllUsers( - [FromQuery] int? limit, - [FromQuery] int? offset - ) - { - if (offset.HasValue && !limit.HasValue) - { - return BadRequest("Limit is required when offset is provided."); - } - - if (limit.HasValue && limit <= 0) - { - return BadRequest("Limit must be greater than zero."); - } - - if (offset.HasValue && offset < 0) - { - return BadRequest("Offset cannot be negative."); - } - - var users = _userService.GetAll(limit, offset); - return Ok(users); - } - - [HttpGet("{id:guid}")] - public IActionResult GetUserById(Guid id) - { - var user = _userService.GetById(id); - return user is null ? NotFound() : Ok(user); - } - - [HttpGet("username/{username}")] - public IActionResult GetUserByUsername(string username) - { - var user = _userService.GetByUsername(username); - return user is null ? NotFound() : Ok(user); - } - - [HttpGet("email/{email}")] - public IActionResult GetUserByEmail(string email) - { - var user = _userService.GetByEmail(email); - return user is null ? NotFound() : Ok(user); - } - - [HttpPost] - public IActionResult CreateUser([FromBody] UserAccount userAccount) - { - if (userAccount.UserAccountId == Guid.Empty) - { - userAccount.UserAccountId = Guid.NewGuid(); - } - - _userService.Add(userAccount); - return CreatedAtAction( - nameof(GetUserById), - new { id = userAccount.UserAccountId }, - userAccount - ); - } - - [HttpPut("{id:guid}")] - public IActionResult UpdateUser( - Guid id, - [FromBody] UserAccount userAccount - ) - { - if ( - userAccount.UserAccountId != Guid.Empty - && userAccount.UserAccountId != id - ) - { - return BadRequest("UserAccountID does not match route id."); - } - - userAccount.UserAccountId = id; - _userService.Update(userAccount); - return NoContent(); - } - - [HttpDelete("{id:guid}")] - public IActionResult DeleteUser(Guid id) - { - _userService.Delete(id); - return NoContent(); - } - } -} diff --git a/WebAPI/Infrastructure/DefaultSqlConnectionFactory.cs b/WebAPI/Infrastructure/DefaultSqlConnectionFactory.cs new file mode 100644 index 0000000..c1c27e5 --- /dev/null +++ b/WebAPI/Infrastructure/DefaultSqlConnectionFactory.cs @@ -0,0 +1,25 @@ +using DataAccessLayer.Sql; +using Microsoft.Data.SqlClient; + +namespace WebAPI.Infrastructure +{ + public class DefaultSqlConnectionFactory : ISqlConnectionFactory + { + private readonly string _connectionString; + + public DefaultSqlConnectionFactory(IConfiguration configuration) + { + _connectionString = + Environment.GetEnvironmentVariable("DB_CONNECTION_STRING") + ?? configuration.GetConnectionString("Default") + ?? throw new InvalidOperationException( + "Database connection string not configured. Set DB_CONNECTION_STRING env var or ConnectionStrings:Default." + ); + } + + public SqlConnection CreateConnection() + { + return new SqlConnection(_connectionString); + } + } +} diff --git a/WebAPI/Program.cs b/WebAPI/Program.cs index 9b595a0..0b78fad 100644 --- a/WebAPI/Program.cs +++ b/WebAPI/Program.cs @@ -1,6 +1,7 @@ using BusinessLayer.Services; -using DataAccessLayer; using DataAccessLayer.Repositories; +using DataAccessLayer.Sql; +using WebAPI.Infrastructure; var builder = WebApplication.CreateBuilder(args); @@ -8,9 +9,11 @@ builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddOpenApi(); + +// Dependency Injection +builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddScoped(); - var app = builder.Build(); if (app.Environment.IsDevelopment())