Solution Explorer
📁 th3chris.API
📄 Program.cs
📄 UsersController.cs
📄 Startup.cs
📁 Controllers
📄 AuthController.cs
📄 ProjectsController.cs
📄 OrdersController.cs
📁 Models
📄 User.cs
📄 Project.cs
📄 Order.cs
📄 BaseEntity.cs
📁 Services
📄 UserService.cs
📄 IUserService.cs
📄 AuthService.cs
📄 EmailService.cs
📁 Data
📄 AppDbContext.cs
📄 DbInitializer.cs
📁 DTOs
📄 UserDto.cs
📄 CreateUserRequest.cs
📄 UpdateUserRequest.cs
📁 Middleware
📄 ErrorHandlingMiddleware.cs
📄 LoggingMiddleware.cs
📁 Extensions
📄 ServiceExtensions.cs
📄 ControllerExtensions.cs
UsersController.cs
// File: UsersController.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;
using th3chris.API.Models;
using th3chris.API.Services;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using System.Security.Claims;
using AutoMapper;
using FluentValidation;
namespace th3chris.API.Controllers
{
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class UsersController : ControllerBase
{
private readonly IUserService _userService;
private readonly ILogger<UsersController> _logger;
private readonly IMapper _mapper;
private readonly IValidator<CreateUserRequest> _createValidator;
private readonly IValidator<UpdateUserRequest> _updateValidator;
public UsersController(
IUserService userService,
ILogger<UsersController> logger,
IMapper mapper,
IValidator<CreateUserRequest> createValidator,
IValidator<UpdateUserRequest> updateValidator)
{
_userService = userService ?? throw new ArgumentNullException(nameof(userService));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
_createValidator = createValidator;
_updateValidator = updateValidator;
}
/// <summary>
/// Retrieves all users with pagination support
/// </summary>
/// <param name="pageNumber">Page number (default: 1)</param>
/// <param name="pageSize">Page size (default: 10, max: 100)</param>
/// <returns>Paginated list of users</returns>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<ActionResult<PagedResult<UserDto>>> GetUsers(
[FromQuery] int pageNumber = 1,
[FromQuery] int pageSize = 10,
[FromQuery] string? search = null,
[FromQuery] string? sortBy = null,
[FromQuery] bool sortDescending = false)
{
try
{
if (pageNumber < 1)
return BadRequest("Page number must be greater than 0");
if (pageSize < 1 || pageSize > 100)
return BadRequest("Page size must be between 1 and 100");
_logger.LogInformation("Getting users - Page: {PageNumber}, Size: {PageSize}, Search: {Search}",
pageNumber, pageSize, search);
var currentUserId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var filter = new UserFilter
{
PageNumber = pageNumber,
PageSize = pageSize,
Search = search,
SortBy = sortBy,
SortDescending = sortDescending,
RequestingUserId = currentUserId
};
var users = await _userService.GetUsersAsync(filter);
var userDtos = _mapper.Map<IEnumerable<UserDto>>(users.Items);
var result = new PagedResult<UserDto>
{
Items = userDtos,
TotalCount = users.TotalCount,
PageNumber = pageNumber,
PageSize = pageSize,
TotalPages = (int)Math.Ceiling((double)users.TotalCount / pageSize)
};
return Ok(result);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occurred while getting users");
return StatusCode(StatusCodes.Status500InternalServerError,
"An error occurred while processing your request");
}
}
/// <summary>
/// Retrieves a specific user by ID
/// </summary>
/// <param name="id">User ID</param>
/// <returns>User details</returns>
[HttpGet("{id:int}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult<UserDetailDto>> GetUser(int id)
{
try
{
if (id <= 0)
return BadRequest("Invalid user ID");
var currentUserId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
_logger.LogInformation("Getting user {UserId} requested by {CurrentUserId}", id, currentUserId);
var user = await _userService.GetUserByIdAsync(id, currentUserId);
if (user == null)
{
_logger.LogWarning("User {UserId} not found or access denied for {CurrentUserId}", id, currentUserId);
return NotFound($"User with ID {id} not found");
}
var userDto = _mapper.Map<UserDetailDto>(user);
return Ok(userDto);
}
catch (UnauthorizedAccessException ex)
{
_logger.LogWarning(ex, "Unauthorized access attempt for user {UserId}", id);
return Forbid();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occurred while getting user {UserId}", id);
return StatusCode(StatusCodes.Status500InternalServerError,
"An error occurred while processing your request");
}
}
/// <summary>
/// Creates a new user
/// </summary>
/// <param name="request">User creation request</param>
/// <returns>Created user</returns>
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task<ActionResult<UserDto>> CreateUser([FromBody] CreateUserRequest request)
{
try
{
if (request == null)
return BadRequest("Request body is required");
var validationResult = await _createValidator.ValidateAsync(request);
if (!validationResult.IsValid)
{
var errors = validationResult.Errors.Select(e => e.ErrorMessage);
return BadRequest(new { Errors = errors });
}
var currentUserId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
_logger.LogInformation("Creating new user {Email} by {CurrentUserId}", request.Email, currentUserId);
var user = _mapper.Map<User>(request);
user.CreatedBy = currentUserId;
user.CreatedAt = DateTime.UtcNow;
var createdUser = await _userService.CreateUserAsync(user);
var userDto = _mapper.Map<UserDto>(createdUser);
return CreatedAtAction(nameof(GetUser), new { id = createdUser.Id }, userDto);
}
catch (DuplicateUserException ex)
{
_logger.LogWarning(ex, "Attempt to create duplicate user {Email}", request?.Email);
return Conflict(new { Message = "User with this email already exists" });
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occurred while creating user");
return StatusCode(StatusCodes.Status500InternalServerError,
"An error occurred while processing your request");
}
}
/// <summary>
/// Updates an existing user
/// </summary>
/// <param name="id">User ID</param>
/// <param name="request">User update request</param>
/// <returns>Updated user</returns>
[HttpPut("{id:int}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult<UserDto>> UpdateUser(int id, [FromBody] UpdateUserRequest request)
{
try
{
if (id <= 0)
return BadRequest("Invalid user ID");
if (request == null)
return BadRequest("Request body is required");
var validationResult = await _updateValidator.ValidateAsync(request);
if (!validationResult.IsValid)
{
var errors = validationResult.Errors.Select(e => e.ErrorMessage);
return BadRequest(new { Errors = errors });
}
var currentUserId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
_logger.LogInformation("Updating user {UserId} by {CurrentUserId}", id, currentUserId);
var existingUser = await _userService.GetUserByIdAsync(id, currentUserId);
if (existingUser == null)
{
_logger.LogWarning("User {UserId} not found for update by {CurrentUserId}", id, currentUserId);
return NotFound($"User with ID {id} not found");
}
_mapper.Map(request, existingUser);
existingUser.UpdatedBy = currentUserId;
existingUser.UpdatedAt = DateTime.UtcNow;
var updatedUser = await _userService.UpdateUserAsync(existingUser);
var userDto = _mapper.Map<UserDto>(updatedUser);
return Ok(userDto);
}
catch (UnauthorizedAccessException ex)
{
_logger.LogWarning(ex, "Unauthorized update attempt for user {UserId}", id);
return Forbid();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occurred while updating user {UserId}", id);
return StatusCode(StatusCodes.Status500InternalServerError,
"An error occurred while processing your request");
}
}
/// <summary>
/// Deletes a user by ID
/// </summary>
/// <param name="id">User ID</param>
/// <returns>No content on success</returns>
[HttpDelete("{id:int}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<IActionResult> DeleteUser(int id)
{
try
{
if (id <= 0)
return BadRequest("Invalid user ID");
var currentUserId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
_logger.LogInformation("Deleting user {UserId} by {CurrentUserId}", id, currentUserId);
var success = await _userService.DeleteUserAsync(id, currentUserId);
if (!success)
{
_logger.LogWarning("User {UserId} not found for deletion by {CurrentUserId}", id, currentUserId);
return NotFound($"User with ID {id} not found");
}
_logger.LogInformation("User {UserId} successfully deleted by {CurrentUserId}", id, currentUserId);
return NoContent();
}
catch (UnauthorizedAccessException ex)
{
_logger.LogWarning(ex, "Unauthorized deletion attempt for user {UserId}", id);
return Forbid();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occurred while deleting user {UserId}", id);
return StatusCode(StatusCodes.Status500InternalServerError,
"An error occurred while processing your request");
}
}
}
}
// File: UserService.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using th3chris.API.Models;
using th3chris.API.Data;
using th3chris.API.Exceptions;
namespace th3chris.API.Services
{
public class UserService : IUserService
{
private readonly AppDbContext _context;
private readonly ILogger<UserService> _logger;
public UserService(AppDbContext context, ILogger<UserService> logger)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<PagedResult<User>> GetUsersAsync(UserFilter filter)
{
var query = _context.Users.AsQueryable();
if (!string.IsNullOrWhiteSpace(filter.Search))
{
var searchTerm = filter.Search.ToLower();
query = query.Where(u => u.Email.ToLower().Contains(searchTerm) ||
u.FirstName.ToLower().Contains(searchTerm) ||
u.LastName.ToLower().Contains(searchTerm));
}
var totalCount = await query.CountAsync();
if (!string.IsNullOrWhiteSpace(filter.SortBy))
{
query = ApplySorting(query, filter.SortBy, filter.SortDescending);
}
else
{
query = query.OrderBy(u => u.LastName).ThenBy(u => u.FirstName);
}
var users = await query
.Skip((filter.PageNumber - 1) * filter.PageSize)
.Take(filter.PageSize)
.ToListAsync();
return new PagedResult<User>
{
Items = users,
TotalCount = totalCount
};
}
}
}
Test Explorer
GetUsers_ReturnsOk
GetUsers_WithPagination
GetUsers_WithSearch
GetUser_ValidId_ReturnsUser
GetUser_InvalidId_NotFound
CreateUser_ValidData
CreateUser_DuplicateEmail
CreateUser_InvalidData
UpdateUser_ValidData
UpdateUser_NotFound
UpdateUser_Unauthorized
DeleteUser_Success
DeleteUser_NotFound
DeleteUser_Unauthorized
Auth_Login_ValidCredentials
Auth_Login_InvalidPassword
Auth_Register_NewUser
Email_SendWelcome_Success
Validation_User_RequiredFields
Validation_Email_Format

Project Inquiry

Faster results — with the right partner.

What you gain

Faster Time-to-Market

{years}+ years of experience means: less trial-and-error, shorter ramp-up time, immediately productive collaboration.

Reduced Project Risk

Clean Architecture, automated tests, CI/CD — technical debt is prevented, not accumulated.

AI as Competitive Advantage

RAG systems, AI agents, LLM integration — not as buzzwords, but as measurable business value in your system.

Instant assessment for your AI project

Describe your project in the chat or upload a document — get an immediate expert assessment of my fit for your project.

Tip: The more specific the description, the more helpful the assessment.

Go to chatSee the RAG pipeline and Smart Booking in action on the homepage.

Trusted by

Hoffmann GroupSchneider ElectricSartoriusRazorfish FrankfurtQuickpacFimat International

25+ years of experience · 13 engagements · 100% remote

How it works

1

Initial Call

Non-binding discussion of requirements, goals, and framework conditions.

2

Proposal

Individual proposal with scope of services, timeline, and terms.

3

Contract

Custom service contract — no fixed price, fair billing based on actual effort.

4

Execution

Regular communication, transparent time tracking, iterative collaboration.

Work Style

100% Remote

Maximum efficiency instead of commute time. On-site visits when there are important reasons.

Digital Communication

Slack, Teams, Zoom, phone, email — whatever you prefer.

Own Infrastructure

Own hardware, tools, and licenses. No additional costs for you.

Contact