Back to: Microservices using ASP.NET Core Web API Tutorials
Implementing User Microservice API Layer
The API Layer is the gateway through which clients interact with the User Microservice. It defines clean, versioned, and secure endpoints for handling all user-facing operations, including registration, login, profile updates, and address management.
Key components include:
- Controllers (e.g., UserController) that handle HTTP requests, invoke application services, and return standardized responses.
- Response Wrappers (ApiResponse<T>) that ensure a consistent API output format.
- Security Middleware that validates JWT tokens and enforces authorization rules.
- Configuration Files (appsettings.json, Program.cs) that define database connections, JWT settings, and service registrations.
The API Layer:
- Validates incoming requests using model binding and data annotations.
- Calls the Application Layer to execute business workflows.
- Manages response formatting, error handling, and HTTP status codes.
- Ensures secure communication via authentication and role-based authorization.
Installing Required Packages
In the UserService.API project, please install the ASP.NET Core Identity package. The following provides support for generating and validating the JWT Token. The UAParser package is required for parsing the User Agent.
- Install-Package Microsoft.AspNetCore.Authentication.JwtBearer
- Install-Package UAParser
First, create a folder named DTOs at the project root directory.
Creating ApiResponse<T> Generic Type for Standard Response
The ApiResponse<T> generic class is designed to provide a uniform structure for all API responses, encapsulating whether the request was successful, the response data (if any), a message string, and a list of error messages. With static helper methods for easily generating success or failure responses, this class promotes consistency across all API endpoints. Create a class file named ApiResponse.cs within the DTOs folder of your UserService.API project and copy and paste the following code:
namespace UserService.API.DTOs { public class ApiResponse<T> { public bool Success { get; set; } public T? Data { get; set; } public string? Message { get; set; } public List<string>? Errors { get; set; } public static ApiResponse<T> SuccessResponse(T data, string? message = null) { return new ApiResponse<T> { Success = true, Data = data, Message = message }; } public static ApiResponse<T> FailResponse(string message, List<string>? errors = null, T? data = default) { return new ApiResponse<T> { Success = false, Data = data, Message = message, Errors = errors }; } } }
Creating User Controller
The UserController class is the central API controller responsible for handling all HTTP requests related to user operations in the User Microservice. It defines endpoint methods for user registration, login, JWT refresh, email confirmation, password management (forgot/reset/change), profile retrieval and update, as well as address management (CRUD).
Each action method receives DTOs as input, delegates business logic to the injected IUserService, and wraps responses in a standardized format, handling both success and error cases. This controller acts as the public interface of your service, translating client requests into service operations and enforcing API-level validation, security, and response consistency.
Create an Empty API Controller named UserController.cs within the Controllers folder of your UserService.API project and copy and paste the following code:
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using System.Net; using System.Security.Claims; using UAParser; using UserService.API.DTOs; using UserService.Application.DTOs; using UserService.Application.Services; namespace UserService.API.Controllers { [ApiController] [Route("api/[controller]")] public class UserController : ControllerBase { private readonly IUserService _userService; public UserController(IUserService userService) { _userService = userService; } [HttpPost("register")] [ProducesResponseType(typeof(ApiResponse<string>), (int)HttpStatusCode.OK)] [ProducesResponseType(typeof(ApiResponse<string>), (int)HttpStatusCode.BadRequest)] public async Task<IActionResult> Register([FromBody] RegisterDTO dto) { try { var result = await _userService.RegisterAsync(dto); if (!result) return BadRequest(ApiResponse<string>.FailResponse("Registration failed. Email or username might already exist.")); return Ok(ApiResponse<string>.SuccessResponse("User registered successfully.")); } catch (Exception ex) { return StatusCode(500, ApiResponse<string>.FailResponse("Error during registration.", new List<string> { ex.Message })); } } [HttpPost("send-confirmation-email")] [ProducesResponseType(typeof(ApiResponse<EmailConfirmationTokenResponseDTO>), (int)HttpStatusCode.OK)] public async Task<IActionResult> SendConfirmationEmail([FromBody] EmailDTO dto) { try { var emailTokenResponse = await _userService.SendConfirmationEmailAsync(dto.Email); if (emailTokenResponse == null) return NotFound(ApiResponse<string>.FailResponse("User with this email not found")); return Ok(ApiResponse<EmailConfirmationTokenResponseDTO>.SuccessResponse(emailTokenResponse, "Email confirmation token generated successfully.")); } catch (Exception ex) { return StatusCode(500, ApiResponse<string>.FailResponse("Error generating confirmation token.", new List<string> { ex.Message })); } } [HttpPost("verify-email")] [ProducesResponseType(typeof(ApiResponse<string>), 200)] [ProducesResponseType(typeof(ApiResponse<string>), 400)] public async Task<IActionResult> VerifyConfirmationEmailAsync([FromBody] ConfirmEmailDTO dto) { try { var success = await _userService.VerifyConfirmationEmailAsync(dto); if (!success) return BadRequest(ApiResponse<string>.FailResponse("Invalid confirmation token or user.")); return Ok(ApiResponse<string>.SuccessResponse("Email confirmed successfully.")); } catch (Exception ex) { return StatusCode(500, ApiResponse<string>.FailResponse("Error confirming email.", new List<string> { ex.Message })); } } [HttpPost("login")] public async Task<IActionResult> Login([FromBody] LoginDTO dto) { var IPAddress = HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""; var UserAgent = GetNormalizedUserAgent(); var loginResponse = await _userService.LoginAsync(dto, IPAddress, UserAgent); // Always return LoginResponseDTO wrapped in ApiResponse if (!string.IsNullOrEmpty(loginResponse.ErrorMessage)) { // Failure case - Success = false, return DTO with error message loginResponse.Succeeded = false; // Add this property if missing return Unauthorized(ApiResponse<LoginResponseDTO>.FailResponse(loginResponse.ErrorMessage, errors: null, data: loginResponse)); } // Success or requires 2FA loginResponse.Succeeded = true; // Make sure this is set on success path as well return Ok(ApiResponse<LoginResponseDTO>.SuccessResponse(loginResponse, loginResponse.RequiresTwoFactor ? "Two-factor authentication required." : "Login successful.")); } [HttpPost("refresh-token")] public async Task<IActionResult> RefreshToken([FromBody] RefreshTokenRequestDTO dto) { var IPAddress = HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""; var UserAgent = GetNormalizedUserAgent(); var refreshTokenResponse = await _userService.RefreshTokenAsync(dto, IPAddress, UserAgent); if (!string.IsNullOrEmpty(refreshTokenResponse.ErrorMessage)) return Unauthorized(ApiResponse<string>.FailResponse(refreshTokenResponse.ErrorMessage)); return Ok(ApiResponse<RefreshTokenResponseDTO>.SuccessResponse(refreshTokenResponse, "Token refreshed successfully.")); } [HttpPost("revoke-token")] [ProducesResponseType(typeof(ApiResponse<string>), (int)HttpStatusCode.OK)] [ProducesResponseType(typeof(ApiResponse<string>), (int)HttpStatusCode.BadRequest)] public async Task<IActionResult> RevokeToken([FromBody] RefreshTokenRequestDTO dto) { try { var success = await _userService.RevokeRefreshTokenAsync(dto.RefreshToken, HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""); if (!success) return BadRequest(ApiResponse<string>.FailResponse("Invalid token or token already revoked.")); return Ok(ApiResponse<string>.SuccessResponse("Token revoked successfully.")); } catch (Exception ex) { return StatusCode(500, ApiResponse<string>.FailResponse("Error revoking token.", new List<string> { ex.Message })); } } [HttpGet("profile/{userId}")] [ProducesResponseType(typeof(ApiResponse<ProfileDTO>), (int)HttpStatusCode.OK)] [ProducesResponseType(typeof(ApiResponse<string>), (int)HttpStatusCode.NotFound)] public async Task<IActionResult> GetProfile(Guid userId) { try { var profile = await _userService.GetProfileAsync(userId); if (profile == null) return NotFound(ApiResponse<string>.FailResponse("User profile not found.")); return Ok(ApiResponse<ProfileDTO>.SuccessResponse(profile)); } catch (Exception ex) { return StatusCode(500, ApiResponse<string>.FailResponse("Error fetching profile.", new List<string> { ex.Message })); } } [HttpPut("profile")] [ProducesResponseType(typeof(ApiResponse<string>), (int)HttpStatusCode.OK)] [ProducesResponseType(typeof(ApiResponse<string>), (int)HttpStatusCode.BadRequest)] public async Task<IActionResult> UpdateProfile([FromBody] UpdateProfileDTO dto) { try { var success = await _userService.UpdateProfileAsync(dto); if (!success) return BadRequest(ApiResponse<string>.FailResponse("Failed to update profile.")); return Ok(ApiResponse<string>.SuccessResponse("Profile updated successfully.")); } catch (Exception ex) { return StatusCode(500, ApiResponse<string>.FailResponse("Error updating profile.", new List<string> { ex.Message })); } } [HttpPost("forgot-password")] [ProducesResponseType(typeof(ApiResponse<string>), (int)HttpStatusCode.OK)] [ProducesResponseType(typeof(ApiResponse<string>), (int)HttpStatusCode.BadRequest)] public async Task<IActionResult> ForgotPassword([FromBody] EmailDTO dto) { try { var forgotPassword = await _userService.ForgotPasswordAsync(dto.Email); if (forgotPassword == null) return NotFound(ApiResponse<string>.FailResponse("Email not found.")); return Ok(ApiResponse<ForgotPasswordResponseDTO>.SuccessResponse(forgotPassword, "Password reset token sent to email.")); } catch (Exception ex) { return StatusCode(500, ApiResponse<string>.FailResponse("Error in forgot password process.", new List<string> { ex.Message })); } } // Reset Password (Forgot Password Flow) [HttpPost("reset-password")] [ProducesResponseType(typeof(ApiResponse<string>), 200)] [ProducesResponseType(typeof(ApiResponse<string>), 400)] public async Task<IActionResult> ResetPassword([FromBody] ResetPasswordDTO dto) { try { var success = await _userService.ResetPasswordAsync(dto.UserId, dto.Token, dto.NewPassword); if (!success) return BadRequest(ApiResponse<string>.FailResponse("Invalid token or user.")); return Ok(ApiResponse<string>.SuccessResponse("Password reset successfully.")); } catch (Exception ex) { return StatusCode(500, ApiResponse<string>.FailResponse("Error resetting password.", new List<string> { ex.Message })); } } [Authorize] [HttpPost("change-password")] [ProducesResponseType(typeof(ApiResponse<string>), 200)] [ProducesResponseType(typeof(ApiResponse<string>), 400)] [ProducesResponseType(typeof(ApiResponse<string>), 401)] public async Task<IActionResult> ChangePassword([FromBody] ChangePasswordDTO dto) { try { var userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; if (string.IsNullOrEmpty(userIdClaim) || !Guid.TryParse(userIdClaim, out var userId)) return Unauthorized(ApiResponse<string>.FailResponse("Invalid user token.")); var success = await _userService.ChangePasswordAsync(userId, dto.CurrentPassword, dto.NewPassword); if (!success) return BadRequest(ApiResponse<string>.FailResponse("Password change failed.")); return Ok(ApiResponse<string>.SuccessResponse("Password changed successfully.")); } catch (Exception ex) { return StatusCode(500, ApiResponse<string>.FailResponse("Error changing password.", new List<string> { ex.Message })); } } [HttpPost("addresses")] [ProducesResponseType(typeof(ApiResponse<string>), (int)HttpStatusCode.OK)] [ProducesResponseType(typeof(ApiResponse<string>), (int)HttpStatusCode.BadRequest)] public async Task<IActionResult> AddOrUpdateAddress([FromBody] AddressDTO dto) { var addressId = await _userService.AddOrUpdateAddressAsync(dto); if (addressId == Guid.Empty) return BadRequest(ApiResponse<string>.FailResponse("Failed to add or update address.")); return Ok(ApiResponse<Guid>.SuccessResponse(addressId, "Address saved successfully.")); } [HttpGet("{userId}/addresses")] [ProducesResponseType(typeof(ApiResponse<IEnumerable<AddressDTO>>), (int)HttpStatusCode.OK)] [ProducesResponseType(typeof(ApiResponse<string>), (int)HttpStatusCode.NotFound)] public async Task<IActionResult> GetAddresses(Guid userId) { try { var addresses = await _userService.GetAddressesAsync(userId); return Ok(ApiResponse<IEnumerable<AddressDTO>>.SuccessResponse(addresses)); } catch (Exception ex) { return StatusCode(500, ApiResponse<string>.FailResponse("Error fetching addresses.", new List<string> { ex.Message })); } } [ProducesResponseType(typeof(ApiResponse<string>), (int)HttpStatusCode.OK)] [ProducesResponseType(typeof(ApiResponse<string>), (int)HttpStatusCode.NotFound)] [HttpPost("delete-address")] public async Task<IActionResult> DeleteAddress([FromBody] DeleteAddressDTO dto) { try { var deleted = await _userService.DeleteAddressAsync(dto.UserId, dto.AddressId); if (!deleted) return BadRequest(ApiResponse<string>.FailResponse("Address not found or deletion failed.")); return Ok(ApiResponse<string>.SuccessResponse("Address deleted successfully.")); } catch (Exception ex) { return StatusCode(500, ApiResponse<string>.FailResponse("Error deleting address.", new List<string> { ex.Message })); } } [HttpGet("{userId}/exists")] public async Task<IActionResult> UserExists(Guid userId) { bool exists = await _userService.IsUserExistsAsync(userId); var response = new ApiResponse<bool> { Success = true, Data = exists, Message = exists ? "User exists." : "User does not exist." }; return Ok(response); } [HttpGet("{userId}/address/{addressId}")] [ProducesResponseType(typeof(ApiResponse<AddressDTO>), (int)HttpStatusCode.OK)] [ProducesResponseType(typeof(ApiResponse<string>), (int)HttpStatusCode.NotFound)] public async Task<IActionResult> GetUserAddress(Guid userId, Guid addressId) { try { var address = await _userService.GetAddressByUserIdAndAddressIdAsync(userId, addressId); if(address != null) return Ok(ApiResponse<AddressDTO>.SuccessResponse(address)); return NotFound(ApiResponse<string>.FailResponse("Address not found.")); } catch (Exception ex) { return StatusCode(500, ApiResponse<string>.FailResponse("Error fetching addresses.", new List<string> { ex.Message })); } } private string GetNormalizedUserAgent() { var userAgentRaw = HttpContext.Request.Headers["User-Agent"].ToString(); if (string.IsNullOrWhiteSpace(userAgentRaw)) return "Unknown"; try { var uaParser = Parser.GetDefault(); ClientInfo clientInfo = uaParser.Parse(userAgentRaw); var browser = clientInfo.UA.Family ?? "UnknownBrowser"; var browserVersion = clientInfo.UA.Major ?? "0"; var os = clientInfo.OS.Family ?? "UnknownOS"; return $"{browser}-{browserVersion}_{os}"; } catch { // In case parsing fails, fallback to raw user agent or unknown return "Unknown"; } } } }
AppSettings.json file:
The appsettings.json file is the primary configuration file for the User Microservice, storing essential settings in a structured JSON format. It includes the connection string needed by Entity Framework Core to connect to the SQL Server database, JWT authentication settings such as the issuer, secret key, and token expiration time, logging configuration, and allowed hosts policy.
These settings are read at runtime by the application and injected where needed, enabling you to manage sensitive information, environment-specific details, and operational parameters without hard-coding them into your source code. So, please modify the appsetings.json as follows of our UserService.API project:
{ "ConnectionStrings": { "UserDbConnection": "Server=LAPTOP-6P5NK25R\\SQLSERVER2022DEV;Database=UserServiceDB;Trusted_Connection=True;TrustServerCertificate=True;" }, "JwtSettings": { "Issuer": "UserService.API", "SecretKey": "fPXxcJw8TW5sA+S4rl4tIPcKk+oXAqoRBo+1s2yjUS4=", "AccessTokenExpirationMinutes": 15 }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*" }
Program.cs (API Configuration)
The Program class serves as the entry point for your ASP.NET Core Web API application. In the context of the User Microservice, it configures controller support, Swagger/OpenAPI documentation, the Entity Framework Core database context, ASP.NET Core Identity for authentication and authorization, and JWT-based security with token validation settings sourced from configuration.
It also registers all required services and repositories (such as IUserRepository and IUserService), applies security middleware, and maps controllers to routes. This class ensures that the application is ready to accept and process HTTP requests according to your service’s design and best practices. So, please modify the Program class file as follows for our UserService.API project:
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using System.Text; using UserService.Application.Services; using UserService.Domain.Repositories; using UserService.Infrastructure.Identity; using UserService.Infrastructure.Persistence; using UserService.Infrastructure.Repositories; namespace UserService.API { public class Program { public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllers() .AddJsonOptions(options => { options.JsonSerializerOptions.PropertyNamingPolicy = null; }); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddDbContext<UserDbContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("UserDbConnection"))); builder.Services.AddIdentity<ApplicationUser, ApplicationRole>(options => { // Password policy options.Password.RequireDigit = true; options.Password.RequiredLength = 8; options.Password.RequireNonAlphanumeric = false; options.Password.RequireUppercase = true; options.Password.RequireLowercase = true; // Lockout settings options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15); options.Lockout.MaxFailedAccessAttempts = 5; options.Lockout.AllowedForNewUsers = true; // User settings (optional) options.User.RequireUniqueEmail = true; }) .AddEntityFrameworkStores<UserDbContext>() .AddDefaultTokenProviders(); // Configure token lifespan (e.g., password reset, email confirmation) builder.Services.Configure<DataProtectionTokenProviderOptions>(opt => { opt.TokenLifespan = TimeSpan.FromHours(3); // Set token validity duration }); //Adding JWT Authentication builder.Services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = false, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = builder.Configuration["JwtSettings:Issuer"], IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["JwtSettings:SecretKey"]!)) }; }); builder.Services.AddScoped<IUserRepository, UserRepository>(); builder.Services.AddScoped<IUserService, UserService.Application.Services.UserService>(); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); app.Run(); } } }
Install Microsoft.EntityFrameworkCore.Design Package in API Project:
In our application,
- The DbContext lives in the Infrastructure project.
- The connection string and appsettings.json live in the API project (the startup project).
- The API project is used as the startup project during migration commands so EF Core tools can pick up configuration and dependency injection setup correctly.
To do this, we need to install Microsoft.EntityFrameworkCore.Design package in the API project (your startup project). This package contains the design-time tools EF Core needs to run migrations and scaffolding.
- Install-Package Microsoft.EntityFrameworkCore.Design
Creating and Applying Database Migration:
In Visual Studio, open the Package Manager Console and execute the Add-Migration and Update-Database commands as follows to generate the Migration file and then apply the Migration file to create the database and required tables. Please execute these commands in the Infrastructure project, which contains the DbContext class.
After executing the above commands, verify the database to ensure it contains the UserServiceDB database with the required tables, as shown in the image below.
The API Layer serves as the public face of the User Microservice, translating external requests into controlled, validated, and secure operations. It promotes a clean separation from the underlying business logic, ensuring that internal changes do not affect client integrations.
The User Microservice plays a crucial role in a microservices architecture by providing a secure, scalable, and maintainable solution for all user-related operations. Its separation as an independent service enables:
- Centralized user data and security management.
- Seamless integration with other microservices through secure APIs.
- Enhanced security via JWT, role-based authorization, and multi-factor authentication.
- Flexible and extensible user management supporting social logins and activity tracking.
Building the User Microservice carefully with clean architecture and security best practices forms a robust backbone for the entire microservices ecosystem, ensuring a consistent and reliable user experience.