User Microservice with ASP.NET Core Web API

User Microservice with ASP.NET Core Web API

In this post, the focus is on developing the User Microservice, a standalone service dedicated to managing all user-related functionalities in a microservices ecosystem. The key functionalities include:

  • User Authentication and Registration: Secure signup, login, and password management.
  • JWT Security: Using JSON Web Tokens for stateless, scalable authentication.
  • Role-Based Authorization: Defining roles (Admin, Customer, Vendor) and enforcing access control.
  • User Profile Management: CRUD operations for user data, including multiple addresses.

By the end of the chapter, you will have implemented the User Service that securely handles authentication, login, and profile management, laying a robust foundation for user identity and access control.

What is the User Domain?

The User Domain is the area of your system that handles all aspects of a user’s lifecycle, including creation, authentication, authorization, profile management, addresses, security, and more. In microservices architecture, we break down a large system into focused, independent services. The User Microservice:

  • Owns all user-related data and operations.
  • Has its own database, API, and business rules.
  • Can be deployed, scaled, and updated independently from other parts of the system.

For a better understanding, please refer to the following image.

User Microservice with ASP.NET Core Web API

Benefits:
  • Independent Scaling: User-related operations (logins, registrations) may have different traffic patterns and performance needs. Isolating the domain lets you scale the service independently.
  • Focused Security: User data is sensitive and requires strict security controls. Separation helps in applying dedicated security and compliance mechanisms.
  • Isolated Deployment: New features or fixes in user management can be deployed without impacting other business services.
  • Clear Boundaries: Encourages clean separation of concerns, improving maintainability and clarity of responsibilities.
Key Features of User Microservice

The User Microservice domain encompasses various subdomains and responsibilities essential for secure, reliable, and flexible user management:

Key Features of User Microservice

User Registration & Authentication
  • Sign Up / Registration: New users can register using details such as username, email, and password. The microservice validates the data to ensure data integrity and prevent duplicate accounts, and may trigger verification steps.
  • Login / Sign In: Users authenticate by providing valid credentials (email, username, mobile number, and password). On success, the service issues JWT tokens for stateless session management.
  • Password Management: Support for password reset, forgotten passwords, and password change, typically via email OTPs or secure links. Always store passwords using secure hashing algorithms (e.g., bcrypt).
  • Multi-Factor Authentication (MFA): Optionally enhances login security by requiring additional verification steps, like OTPs sent via SMS/email or authenticator apps (Google Authenticator, Authy).
User Profile Management
  • CRUD Operations: Users (or admins) can create, view, update, and delete user profile information.
  • Profile Data: Stores user details, including name, email, phone number, profile photo, preferences, and other metadata.
  • Address Book: Store multiple addresses per user (shipping, billing, etc.), with the ability to add, edit, or remove them.
  • Default Address Selection: Users can mark certain addresses as their default for faster checkout or communication.
Role and Permission Management
  • Role Definition: Roles categorize users based on permissions, e.g., Admins have full access, Customers have shopping-related permissions, and Vendors can manage products. Typical roles: Admin, Customer, Vendor, each with its own set of permissions.
  • Role-Based Access Control (RBAC): The service restricts access to endpoints based on the user’s assigned role. For example, only Admins can manage other users.
  • Authorization Policies: Define business policies, such as who can edit profiles, view sensitive data, or perform admin tasks, enhancing security and flexibility.
Session Management and Security
  1. JWT Authentication: After logging in, a JWT token is generated and sent to the client, which uses it in subsequent requests. This enables stateless authentication (no need to store session info on the server side).
  2. Refresh Tokens: Supports token refresh mechanisms to securely extend user sessions without requiring frequent logins.
  3. Password Hashing: Uses strong, one-way hashing algorithms to store passwords securely.
  4. Account Lockout: If multiple failed login attempts are detected, the account will be temporarily locked to prevent brute-force attacks.
User Activity Tracking
  • Login Tracking: Maintains a history of user logins with metadata, including login times, IP addresses, device information, and browser details, to detect unusual patterns or potential breaches. This is helpful for auditing and alerting users to suspicious logins.
  • Last Seen: Tracks last active times, useful for features like “last online” indicators or session timeout enforcement.
  • Suspicious Activity Alerts: Detect and notify users and administrators about suspicious activity (e.g., multiple failed logins, logins from new locations).
Account Verification
  • Email Verification: Upon registration or email update, send a verification email to confirm the address via OTP or a confirmation link, thereby enhancing account authenticity.
  • Phone Number Verification: Send an OTP (one-time password) via SMS to verify mobile numbers.
  • Activation Status: Users can only access the platform once their account is verified and active. Maintains user account status (active/inactive/blocked) to control access.
API for User Data Access
  • Secure APIs: Other microservices (such as Order, Payment, and Notification) can request user information for processing transactions, personalizing experiences, or verifying identity. Therefore, it provides REST endpoints with appropriate authentication and authorization for other microservices to retrieve necessary user data (e.g., the Order Microservice fetching shipping information).
Recommended Technology Stack for Building a Modern User Microservice

Let us understand the best technology stack for implementing a secure and scalable User Microservice. This approach combines ASP.NET Core Web API, Entity Framework Core, ASP.NET Core Identity, and JWT authentication to deliver robust user management, seamless data access, and stateless security, making your microservices architecture modern, efficient, and ready for production.

Recommended Technology Stack for Building a Modern User Microservice

Framework: ASP.NET Core Web API
  • ASP.NET Core Web API is Microsoft’s modern, cross-platform framework designed for building RESTful APIs.
  • It supports the latest .NET versions (currently, .NET 8 LTS is the latest Long-Term Support version), ensuring performance, security, and ongoing support.
  • ASP.NET Core Web API offers built-in dependency injection, middleware pipelines, configuration systems, logging, and great integration with authentication and authorization mechanisms.
  • It is ideal for microservices due to its lightweight, modular, and scalable design.

Real-World Benefit: You can run your service on Windows, Linux, or containers (Docker, Kubernetes) with full Microsoft support and a vibrant ecosystem.

ORM: Entity Framework Core (EF Core)
  • Entity Framework Core is Microsoft’s official Object-Relational Mapper (ORM) for .NET applications.
  • EF Core enables developers to interact with relational databases (such as SQL Server, PostgreSQL, MySQL, and SQLite) using strongly typed .NET objects.
  • It supports Code-First and Database-First approaches, making it flexible for various project needs.
  • EF Core automatically manages database schema creation, migrations, and relationships between entities.
  • In the User Microservice, EF Core persists user data, roles, claims, and related profile information securely and efficiently.

Real-World Benefit: EF Core accelerates development, reduces boilerplate, and keeps your code maintainable as your data model evolves.

Membership Management: ASP.NET Core Identity

ASP.NET Core Identity is the standard framework for handling all user-related functionalities:

  • User Registration: It provides built-in mechanisms for signing up users, including validation for unique usernames and emails.
  • Login and Password Management: Handles secure user login and password operations, including password resets.
  • Password Hashing: Uses industry-standard hashing algorithms (e.g., PBKDF2) to securely store passwords, protecting against password leaks.
  • Role and Claims Management: Enables assigning users to roles (e.g., Admin, Customer) and attaching claims (permissions or attributes) to users.
  • Security Best Practices: Implement features such as account lockout after multiple failed attempts, two-factor authentication (2FA), and email confirmation workflows.
  • Extensibility: You can extend the IdentityUser class to include custom user profile fields, such as address, phone number, and profile pictures.

Real-World Benefit: You get a mature, tested security system that’s customizable to your business needs, with rapid development and proven best practices.

Authentication: JWT (JSON Web Tokens) for Stateless Authentication

JWT is a compact, URL-safe token format that encodes claims about the user (such as identity, roles, and permissions) and is digitally signed to prevent tampering.

Instead of traditional cookie-based authentication (good for web apps), APIs prefer stateless token authentication. Upon successful login, ASP.NET Core Identity can be configured to generate a JWT access token, which clients (web/mobile apps) use for subsequent API calls.

Key Properties:
  • Statelessness: JWT tokens contain all necessary user claims and do not require server-side session storage, enabling easy horizontal scaling.
  • Scalability: Multiple microservices can validate JWT tokens independently without a central session store.
  • Interoperability: JWTs are widely supported across platforms and can be easily passed via HTTP headers.
  • Security: Tokens are signed (and optionally encrypted), ensuring data integrity and authenticity.
  • Compatibility: Works well with API Gateways and inter-service communication by passing tokens securely for authorization.
  • Standard: Supported everywhere, web, mobile, desktop, IoT.

Real-World Benefit: With JWT, your User Microservice can authenticate users once, issue a token, and let any microservice (Order, Payment, etc.) verify the user by simply checking the token; no central session store or database call is required.

Installing the Required Packages for User Service:

Let us start by implementing the Required Packages for user service, including Identity, JWT Token, and EF Core for SQL Server Database.

UserService.Domain

In the Domain Project, please install the following package so that we can use the EF Core-related attributes directly on entities.

  • Install-Package Microsoft.EntityFrameworkCore
UserService.Infrastructure

In the UserService.Infrastructure class library project, please install the Identity packages. ASP.NET Core Identity needs 4 packages to be installed. These packages are as follows:

  • Microsoft.AspNetCore.Identity.EntityFrameworkCore: This package integrates ASP.NET Core Identity with Entity Framework Core. It provides the necessary implementation to store user, role, and other identity-related data in a database using EF Core. 
  • Microsoft.EntityFrameworkCore.SqlServer: This package is the Entity Framework Core database provider for Microsoft SQL Server. It allows the application to use Entity Framework Core to interact with SQL Server.
  • Microsoft.EntityFrameworkCore.Tools: This package provides support for database migration. 
  • Microsoft.AspNetCore.Identity: This provides the core identity support in ASP.NET Core.

So, open the Package Manager Console and then execute the following commands to install the above packages:

  • Install-Package Microsoft.AspNetCore.Identity.EntityFrameworkCore
  • Install-Package Microsoft.EntityFrameworkCore.SqlServer
  • Install-Package Microsoft.EntityFrameworkCore.Tools
  • Install-Package Microsoft.AspNetCore.Identity
UserService.API

In the UserService.API project, please install the ASP.NET Core Identity package. The following provides support for generating and validating JWT Tokens. The UAParser package is required for parsing the User Agent.

  • Install-Package Microsoft.AspNetCore.Authentication.JwtBearer
  • Install-Package UAParser

Implementing Domain Layer (UserService.Domain):

The Domain Layer is responsible for representing the core business logic and rules of the User Microservice, independent of any technical frameworks or infrastructure concerns.

  • It defines the essential entities (such as User, Address, and RefreshToken), value objects, and repository interfaces that express what the business does, including user registration, authentication, and profile management, without specifying how these operations are implemented.
  • By isolating the business model and rules here, this layer ensures your application remains maintainable, testable, and adaptable to future changes or new technologies.

First, create two folders at the project root directory named ‘Entities’ and ‘Repositories’.

Entities/User.cs

Create a class file named User.cs within the Entities folder of your UserService.Domain project and copy and paste the following code. The User entity represents the core user profile in the domain model, encapsulating all essential user information, including a unique ID, username, email, phone number, full name, profile photo URL, email confirmation status, and activation status, as well as timestamps for creation and last login, and a collection of associated addresses.

using Microsoft.EntityFrameworkCore;
namespace UserService.Domain.Entities
{
    [Index(nameof(Email), Name = "Index_RegistrationNumber", IsUnique = true)]
    public class User
    {
        public Guid Id { get; set; }
        public string? UserName { get; set; } = null!;
        public string? Email { get; set; } = null!;
        public bool IsEmailConfirmed { get; set; }
        public bool IsActive { get; set; }
        public string? PhoneNumber { get; set; }
        public string? FullName { get; set; }
        public string? ProfilePhotoUrl { get; set; }
        public DateTime CreatedAt { get; set; }
        public DateTime? LastLoginAt { get; set; }
        public List<Address> Addresses { get; set; } = new();
    }
}
Entities/Address.cs

Create a class file named Address.cs within the Entities folder of your UserService.Domain project and copy and paste the following code. The Address entity models the physical address information associated with a user, including properties for a unique ID, the owning user ID, address lines, city, state, postal code, country, and boolean flags to indicate whether the address is default for shipping or billing. This structure enables each user to manage multiple addresses, supporting e-commerce shipping and billing requirements.

using Microsoft.EntityFrameworkCore;
namespace UserService.Domain.Entities
{
    [Index(nameof(Email), Name = "Index_Email_Unique", IsUnique = true)]
    public class User
    {
        public Guid Id { get; set; }
        public string? UserName { get; set; } = null!;
        public string? Email { get; set; } = null!;
        public bool IsEmailConfirmed { get; set; }
        public bool IsActive { get; set; }
        public string? PhoneNumber { get; set; }
        public string? FullName { get; set; }
        public string? ProfilePhotoUrl { get; set; }
        public DateTime CreatedAt { get; set; }
        public DateTime? LastLoginAt { get; set; }
        public List<Address> Addresses { get; set; } = new();
    }
}
Entities/Client.cs

Create a class file named Client.cs within the Entities folder of your UserService.Domain project and copy and paste the following code. This entity with hold the client information.

using Microsoft.EntityFrameworkCore;
namespace UserService.Domain.Entities
{
    [Index(nameof(ClientName), Name = "Index_ClientName_Unique", IsUnique = true)]
    public class Client
    {
        public string ClientId { get; set; } = null!;
        public string ClientName { get; set; } = null!; // e.g., "Web", "Android", "iOS"
        public string? Description { get; set; }
        public bool IsActive { get; set; }
        public ICollection<RefreshToken> RefreshTokens { get; set; } = new List<RefreshToken>();
    }
}
Entities/RefreshToken.cs

Create a class file named RefreshToken.cs within the Entities folder of your UserService.Domain project and copy and paste the following code. The RefreshToken entity represents a security token used to renew JWT access tokens without requiring the user to re-login. It contains the unique ID, the associated UserId, the token string, timestamps for creation, expiry, and optional revocation, as well as IP tracking for both creation and revocation events. It includes logic to determine if a token is active or expired, strengthening session security.

namespace UserService.Domain.Entities
{
    public class RefreshToken
    {
        public Guid Id { get; set; }
        public Guid UserId { get; set; }
        public string Token { get; set; } = null!;
        public string ClientId { get; set; } = null!;    // Web/Android/iOS etc.
        public Client Client { get; set; } = null!;
        public string UserAgent { get; set; } = null!;   // Chrome, Firefox, Safari, etc.
        public DateTime CreatedAt { get; set; }
        public string CreatedByIp { get; set; } = null!;
        public DateTime? RevokedAt { get; set; }
        public string? RevokedByIp { get; set; }
        public DateTime ExpiresAt { get; set; }
        public bool IsActive => RevokedAt == null && !IsExpired;
        public bool IsExpired => DateTime.UtcNow >= ExpiresAt;
    }
}
Repositories/IUserRepository.cs

The IUserRepository interface defines the contract for all user-related data access and manipulation operations in the domain, including finding users by email, username, or ID, creating users, validating passwords, updating user data, managing user roles, handling email and password tokens, login tracking, lockout and two-factor authentication checks, refresh token lifecycle management, and address management. By abstracting these operations, it decouples domain logic from data persistence, supporting both testability and extensibility.

Create a class file named IUserRepository.cs within the Repositories folder of your UserService.Domain project and copy and paste the following code:

using UserService.Domain.Entities;
namespace UserService.Domain.Repositories
{
    public interface IUserRepository
    {
        Task<User?> FindByEmailAsync(string email);
        Task<User?> FindByUserNameAsync(string userName);
        Task<User?> FindByIdAsync(Guid id);
        Task<bool> CreateUserAsync(User user, string password);
        Task<bool> CheckPasswordAsync(User user, string password);
        Task<bool> UpdateUserAsync(User user);
        Task<IList<string>> GetUserRolesAsync(User user);
        Task<bool> AddUserToRoleAsync(User user, string role);
        Task<bool> VerifyConfirmaionEmailAsync(User user, string token);
        Task<string?> GenerateEmailConfirmationTokenAsync(User user);
        Task<string?> GeneratePasswordResetTokenAsync(User user);
        Task<bool> ResetPasswordAsync(User user, string token, string newPassword);
        Task<bool> ChangePasswordAsync(User user, string currentPassword, string newPassword);
        Task UpdateLastLoginAsync(User user, DateTime loginTime);
        Task<string> GenerateAndStoreRefreshTokenAsync(Guid userId, string clientId, string userAgent, string ipAddress);
        Task<RefreshToken?> GetRefreshTokenAsync(string token);
        Task RevokeRefreshTokenAsync(RefreshToken refreshToken, string ipAddress);
        Task<List<Address>> GetAddressesByUserIdAsync(Guid userId);
        Task<bool> AddOrUpdateAddressAsync(Address address);
        Task<bool> DeleteAddressAsync(Guid userId, Guid addressId);
        Task<bool> IsLockedOutAsync(User user);
        Task<bool> IsTwoFactorEnabledAsync(User user);
        Task IncrementAccessFailedCountAsync(User user);
        Task ResetAccessFailedCountAsync(User user);
        Task<DateTime?> GetLockoutEndDateAsync(User user);
        Task<int> GetMaxFailedAccessAttemptsAsync();
        Task<int> GetAccessFailedCountAsync(User user);
        Task<bool> IsValidClientAsync(string clientId);
    }
}

Implementing Infrastructure Layer (UserService.Infrastructure)

The Infrastructure Layer provides the technical implementation details necessary to fulfill the contracts defined in the domain layer, including actual database access, integration with ASP.NET Core Identity, and external service integration.

  • It contains classes such as UserDbContext for Entity Framework Core, concrete repository implementations, and custom Identity entities (ApplicationUser and ApplicationRole).
  • This layer bridges the gap between abstract business logic and real-world technical concerns, enabling the persistence of data, handling authentication, and communicating with other systems while keeping infrastructure details separate from business logic.

First, create 3 folders at the project root directory named Identity, Persistence, and Repositories.

Identity/ApplicationUser.cs

The ApplicationUser class extends the ASP.NET Core Identity framework’s IdentityUser<Guid> and augments it with additional application-specific fields such as creation date, activation status, full name, profile photo, email confirmation status, last login timestamp, and navigation collections for addresses and refresh tokens. This class directly maps to the underlying database and is integral to ASP.NET Identity’s authentication and authorization system.

Create a class file named ApplicationUser.cs within the Identity folder of your UserService.Infrastructure project and copy and paste the following code.

using Microsoft.AspNetCore.Identity;
using UserService.Domain.Entities;

namespace UserService.Infrastructure.Identity
{
    //Here, Guid will be the data type of Primary column in the Roles table
    //You can also specify String, Integer 
    public class ApplicationUser : IdentityUser<Guid>
    {
        public DateTime CreatedAt { get; set; }
        public bool IsActive { get; set; } = true;
        public string? FullName { get; set; }
        public string? ProfilePhotoUrl { get; set; }
        public bool IsEmailConfirmed { get; set; }
        public DateTime? LastLoginAt { get; set; }
        public ICollection<Address> Addresses { get; set; } = new List<Address>();
        public ICollection<RefreshToken> RefreshTokens { get; set; } = new List<RefreshToken>();
    }
}
Identity/ApplicationRole.cs

The ApplicationRole class extends IdentityRole<Guid> and allows for additional role metadata, notably a description field, enabling more meaningful and descriptive role definitions (e.g., Admin, Customer, Vendor) used for role-based access control within the system.

Create a class file named ApplicationRole.cs within the Identity folder of your UserService.Infrastructure project and copy and paste the following code:

using Microsoft.AspNetCore.Identity;
namespace UserService.Infrastructure.Identity
{
    //Here, Guid will be the data type of the Primary column in the Roles table
    //You can also specify String, Integer 
    public class ApplicationRole : IdentityRole<Guid>
    {
        public string? Description { get; set; }
    }
}
Persistence/UserDbContext.cs

The UserDbContext class inherits from IdentityDbContext<ApplicationUser, ApplicationRole, Guid> and serves as the Entity Framework Core database context for the User Microservice. It configures mappings for the user, role, address, and refresh token tables, manages their relationships and seed data, and provides the gateway for all database operations.

Create a class file named UserDbContext.cs within the Persistence folder of your UserService.Infrastructure project and copy and paste the following code:

using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using UserService.Infrastructure.Identity;
using UserService.Domain.Entities;

namespace UserService.Infrastructure.Persistence
{
    //The third type parameter (Guid) in IdentityDbContext<ApplicationUser, ApplicationRole, Guid>
    //specifies the type of the primary key used by all Identity entities (users, roles, claims, etc.).
    public class UserDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, Guid>
    {
        public UserDbContext(DbContextOptions<UserDbContext> options) : base(options) { }

        public DbSet<Address> Addresses { get; set; } = null!;
        public DbSet<RefreshToken> RefreshTokens { get; set; } = null!;
        public DbSet<Client> Clients { get; set; } = null!;
        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);

            builder.Entity<RefreshToken>(entity =>
            {
                // Configure foreign key explicitly, ensure EF uses the existing UserId column:
                entity.HasOne<ApplicationUser>() // Navigation to ApplicationUser
                    .WithMany(u => u.RefreshTokens) // Collection navigation on ApplicationUser
                    .HasForeignKey(rt => rt.UserId) // Explicit FK property
                    .IsRequired()
                    .OnDelete(DeleteBehavior.Cascade);
            });

            builder.Entity<Address>(entity =>
            {
                entity.HasOne<ApplicationUser>()      // Navigation to ApplicationUser
                    .WithMany(u => u.Addresses)       // Collection navigation on ApplicationUser
                    .HasForeignKey(a => a.UserId)     // Explicit FK property
                    .IsRequired()
                    .OnDelete(DeleteBehavior.Cascade);
            });

            // Rename Identity tables here:
            builder.Entity<ApplicationUser>().ToTable("Users");
            builder.Entity<ApplicationRole>().ToTable("Roles");
            builder.Entity<IdentityUserRole<Guid>>().ToTable("UserRoles");
            builder.Entity<IdentityUserClaim<Guid>>().ToTable("UserClaims");
            builder.Entity<IdentityUserLogin<Guid>>().ToTable("UserLogins");
            builder.Entity<IdentityRoleClaim<Guid>>().ToTable("RoleClaims");
            builder.Entity<IdentityUserToken<Guid>>().ToTable("UserTokens");

            var adminRoleId = Guid.Parse("c4a3298c-6198-4d12-bd1a-56d1d1ce0aa7");
            var customerRoleId = Guid.Parse("38b657f4-ac20-4a5c-b2a3-16dfad61c381");
            var vendorRoleId = Guid.Parse("582880c3-f554-490f-a24e-526db35cffa5");

            builder.Entity<ApplicationRole>().HasData(
               new ApplicationRole
               {
                   Id = adminRoleId,
                   Name = "Admin",
                   NormalizedName = "ADMIN",
                   Description = "Administrator with full permissions"
               },
               new ApplicationRole
               {
                   Id = customerRoleId,
                   Name = "Customer",
                   NormalizedName = "CUSTOMER",
                   Description = "Customer with shopping permissions"
               },
               new ApplicationRole
               {
                   Id = vendorRoleId,
                   Name = "Vendor",
                   NormalizedName = "VENDOR",
                   Description = "Vendor who can manage products"
               }
           );

            builder.Entity<Client>().HasData(
                new Client { ClientId = "web", ClientName = "Web Client", Description = "Web browser clients", IsActive = true },
                new Client { ClientId = "android", ClientName = "Android Client", Description = "Android mobile app", IsActive = true },
                new Client { ClientId = "ios", ClientName = "iOS Client", Description = "iOS mobile app", IsActive = true }
            );
        }
    }
}

Note: It is not possible to specify different primary key types for different Identity tables within the same IdentityDbContext generic parameters.

Repositories/UserRepository.cs

The UserRepository class implements IUserRepository and contains the concrete logic for all user data operations using Entity Framework Core and ASP.NET Identity, including user creation, retrieval, authentication checks, role management, token handling, address operations, and security functions such as lockout and access failure counting. It serves as the infrastructure bridge between the domain logic and the persistent storage.

Create a class file named UserRepository.cs within the Repositories folder of your UserService.Infrastructure project and copy and paste the following code:

using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using UserService.Domain.Entities;
using UserService.Domain.Repositories;
using UserService.Infrastructure.Identity;
using UserService.Infrastructure.Persistence;
namespace UserService.Infrastructure.Repositories
{
public class UserRepository : IUserRepository
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly UserDbContext _dbContext;
public UserRepository(
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
UserDbContext dbContext)
{
_userManager = userManager;
_dbContext = dbContext;
}
private User MapToDomain(ApplicationUser appUser)
{
if (appUser == null) return null!;
return new User
{
Id = appUser.Id,
UserName = appUser.UserName,
Email = appUser.Email,
FullName = appUser.FullName,
PhoneNumber = appUser.PhoneNumber,
ProfilePhotoUrl = appUser.ProfilePhotoUrl,
IsActive = appUser.IsActive,
CreatedAt = appUser.CreatedAt,
LastLoginAt = appUser.LastLoginAt,
IsEmailConfirmed = appUser.EmailConfirmed
};
}
private ApplicationUser MapToApplicationUser(User user)
{
return new ApplicationUser
{
Id = user.Id,
UserName = user.UserName,
Email = user.Email,
FullName = user.FullName,
PhoneNumber = user.PhoneNumber,
ProfilePhotoUrl = user.ProfilePhotoUrl,
IsActive = user.IsActive,
CreatedAt = user.CreatedAt,
LastLoginAt = user.LastLoginAt,
EmailConfirmed = user.IsEmailConfirmed
};
}
public async Task<User?> FindByEmailAsync(string email)
{
var appUser = await _userManager.FindByEmailAsync(email);
if (appUser == null)
return null;
return MapToDomain(appUser);
}
public async Task<User?> FindByUserNameAsync(string userName)
{
var appUser = await _userManager.FindByNameAsync(userName);
if (appUser == null)
return null;
return MapToDomain(appUser);
}
public async Task<User?> FindByIdAsync(Guid id)
{
var appUser = await _userManager.FindByIdAsync(id.ToString());
if (appUser == null)
return null;
return MapToDomain(appUser);
}
public async Task<bool> CreateUserAsync(User user, string password)
{
var appUser = MapToApplicationUser(user);
var result = await _userManager.CreateAsync(appUser, password);
return result.Succeeded;
}
public async Task<bool> CheckPasswordAsync(User user, string password)
{
var appUser = await _userManager.FindByIdAsync(user.Id.ToString());
if (appUser == null) 
return false;
return await _userManager.CheckPasswordAsync(appUser, password);
}
public async Task<bool> UpdateUserAsync(User user)
{
var appUser = await _userManager.FindByIdAsync(user.Id.ToString());
if (appUser == null) 
return false;
appUser.UserName = user.UserName;
appUser.Email = user.Email;
appUser.FullName = user.FullName;
appUser.PhoneNumber = user.PhoneNumber;
appUser.ProfilePhotoUrl = user.ProfilePhotoUrl;
var result = await _userManager.UpdateAsync(appUser);
return result.Succeeded;
}
public async Task<IList<string>> GetUserRolesAsync(User user)
{
var appUser = await _userManager.FindByIdAsync(user.Id.ToString());
if (appUser == null) 
return new List<string>();
return await _userManager.GetRolesAsync(appUser);
}
public async Task<bool> AddUserToRoleAsync(User user, string role)
{
var appUser = await _userManager.FindByIdAsync(user.Id.ToString());
if (appUser == null) 
return false;
var result = await _userManager.AddToRoleAsync(appUser, role);
return result.Succeeded;
}
public async Task<string?> GenerateEmailConfirmationTokenAsync(User user)
{
var appUser = await _userManager.FindByIdAsync(user.Id.ToString());
if (appUser == null)
return null;
return await _userManager.GenerateEmailConfirmationTokenAsync(appUser);
}
public async Task<bool> VerifyConfirmaionEmailAsync(User user, string token)
{
var appUser = await _userManager.FindByIdAsync(user.Id.ToString());
if (appUser == null) 
return false;
var result = await _userManager.ConfirmEmailAsync(appUser, token);
return result.Succeeded;
}
public async Task<string?> GeneratePasswordResetTokenAsync(User user)
{
var appUser = await _userManager.FindByIdAsync(user.Id.ToString());
if (appUser == null)
return null;
var token = await _userManager.GeneratePasswordResetTokenAsync(appUser);
return token;  
}
public async Task<bool> ResetPasswordAsync(User user, string token, string newPassword)
{
var appUser = await _userManager.FindByIdAsync(user.Id.ToString());
if (appUser == null)
return false;
var result = await _userManager.ResetPasswordAsync(appUser, token, newPassword);
return result.Succeeded;
}
public async Task<bool> ChangePasswordAsync(User user, string currentPassword, string newPassword)
{
var appUser = await _userManager.FindByIdAsync(user.Id.ToString());
if (appUser == null) 
return false;
var result = await _userManager.ChangePasswordAsync(appUser, currentPassword, newPassword);
return result.Succeeded;
}
public async Task UpdateLastLoginAsync(User user, DateTime loginTime)
{
var appUser = await _userManager.FindByIdAsync(user.Id.ToString());
if (appUser == null) return;
appUser.LastLoginAt = loginTime;
await _userManager.UpdateAsync(appUser);
}
public async Task<string> GenerateAndStoreRefreshTokenAsync(Guid userId, string clientId, string userAgent, string ipAddress)
{
// Revoke existing tokens for this user/client/useragent before issuing a new one
await RevokeAllRefreshTokensAsync(userId, clientId, userAgent, ipAddress);
var refreshToken = new RefreshToken
{
Id = Guid.NewGuid(),
UserId = userId,
ClientId = clientId,
UserAgent = userAgent,
Token = Convert.ToBase64String(Guid.NewGuid().ToByteArray()),
CreatedAt = DateTime.UtcNow,
ExpiresAt = DateTime.UtcNow.AddDays(7),
CreatedByIp = ipAddress
};
_dbContext.RefreshTokens.Add(refreshToken);
await _dbContext.SaveChangesAsync();
return refreshToken.Token;
}
public async Task<RefreshToken?> GetRefreshTokenAsync(string token)
{
return await _dbContext.RefreshTokens.FirstOrDefaultAsync(rt => rt.Token == token);
}
public async Task RevokeRefreshTokenAsync(RefreshToken refreshToken, string ipAddress)
{
refreshToken.RevokedAt = DateTime.UtcNow;
refreshToken.RevokedByIp = ipAddress;
await _dbContext.SaveChangesAsync();
}
public async Task<List<Address>> GetAddressesByUserIdAsync(Guid userId)
{
return await _dbContext.Addresses.Where(a => a.UserId == userId).ToListAsync();
}
public async Task<bool> AddOrUpdateAddressAsync(Address address)
{
var existing = await _dbContext.Addresses.FindAsync(address.Id);
if (existing == null)
{
await _dbContext.Addresses.AddAsync(address);
}
else
{
existing.AddressLine1 = address.AddressLine1;
existing.AddressLine2 = address.AddressLine2;
existing.City = address.City;
existing.State = address.State;
existing.PostalCode = address.PostalCode;
existing.Country = address.Country;
existing.IsDefaultBilling = address.IsDefaultBilling;
existing.IsDefaultShipping = address.IsDefaultShipping;
}
await _dbContext.SaveChangesAsync();
return true;
}
public async Task<bool> DeleteAddressAsync(Guid userId, Guid addressId)
{
var address = await _dbContext.Addresses.FirstOrDefaultAsync(a => a.Id == addressId && a.UserId == userId);
if (address == null) 
return false;
_dbContext.Addresses.Remove(address);
await _dbContext.SaveChangesAsync();
return true;
}
public async Task<bool> IsLockedOutAsync(User user)
{
var appUser = await _userManager.FindByIdAsync(user.Id.ToString());
return appUser != null && await _userManager.IsLockedOutAsync(appUser);
}
public async Task<bool> IsTwoFactorEnabledAsync(User user)
{
var appUser = await _userManager.FindByIdAsync(user.Id.ToString());
return appUser != null && await _userManager.GetTwoFactorEnabledAsync(appUser);
}
public async Task IncrementAccessFailedCountAsync(User user)
{
var appUser = await _userManager.FindByIdAsync(user.Id.ToString());
if (appUser != null) 
await _userManager.AccessFailedAsync(appUser);
}
public async Task ResetAccessFailedCountAsync(User user)
{
var appUser = await _userManager.FindByIdAsync(user.Id.ToString());
if (appUser != null) 
await _userManager.ResetAccessFailedCountAsync(appUser);
}
public async Task<DateTime?> GetLockoutEndDateAsync(User user)
{
var appUser = await _userManager.FindByIdAsync(user.Id.ToString());
if (appUser == null)
return null;
// LockoutEnd can be null, so return nullable DateTime
return appUser.LockoutEnd?.UtcDateTime;
}
public Task<int> GetMaxFailedAccessAttemptsAsync()
{
return Task.FromResult(_userManager.Options.Lockout.MaxFailedAccessAttempts);
}
public async Task<int> GetAccessFailedCountAsync(User user)
{
var appUser = await _userManager.FindByIdAsync(user.Id.ToString());
return appUser?.AccessFailedCount ?? 0;
}
public async Task<bool> IsValidClientAsync(string clientId)
{
return await _dbContext.Clients.AnyAsync(c => c.ClientId == clientId);
}
//To Remove the Refresh Tokens
//private async Task RemoveAllRefreshTokensAsync(Guid userId, string clientId, string userAgent)
//{
//    var tokens = _dbContext.RefreshTokens
//        .Where(t => t.UserId == userId 
//            && t.ClientId == clientId 
//            && t.UserAgent == userAgent 
//            && t.RevokedAt == null);
//    _dbContext.RefreshTokens.RemoveRange(tokens);
//    await _dbContext.SaveChangesAsync();
//}
private async Task RevokeAllRefreshTokensAsync(Guid userId, string clientId, string userAgent, string revokedByIp)
{
var tokens = await _dbContext.RefreshTokens
.Where(t => t.UserId == userId
&& t.ClientId == clientId
&& t.UserAgent == userAgent
&& t.RevokedAt == null)
.ToListAsync();
foreach (var token in tokens)
{
token.RevokedAt = DateTime.UtcNow;
token.RevokedByIp = revokedByIp;
}
await _dbContext.SaveChangesAsync();
}
}
}

Implementing Application Layer (UserService.Application)

The Application Layer handles the use cases and business workflows of our service, acting as an intermediary between the domain logic and the external API or presentation layer.

  • It defines service interfaces and their implementations (like IUserService and UserService), application-specific DTOs for request/response handling, and business workflow coordination, such as user registration, login, token management, or profile updates.
  • This layer enforces application-level policies, coordinates domain objects, and ensures that business logic is executed correctly in response to incoming client requests.

First, create two folders at the project root directory named ‘DTOs’ and ‘Services’.

DTOs/RegisterDTO.cs

Create a class file named RegisterDTO.cs within the DTOs folder of your UserService.Application project and copy and paste the following code. The RegisterDTO class is designed to encapsulate all the data required for new user registration within the User Microservice. It typically includes fields such as UserName, Email, Password, and optional properties like PhoneNumber and FullName, allowing the API to receive a new user’s information in a structured, validated manner for account creation.

using System.ComponentModel.DataAnnotations;
namespace UserService.Application.DTOs
{
public class RegisterDTO
{
[Required(ErrorMessage = "Username is required.")]
[StringLength(50, MinimumLength = 3, ErrorMessage = "Username must be between 3 and 50 characters.")]
public string UserName { get; set; } = null!;
[Required(ErrorMessage = "Email is required.")]
[EmailAddress(ErrorMessage = "Invalid email address format.")]
public string Email { get; set; } = null!;
[Required(ErrorMessage = "Password is required.")]
[StringLength(100, MinimumLength = 8, ErrorMessage = "Password must be at least 8 characters.")]
public string Password { get; set; } = null!;
[Phone(ErrorMessage = "Invalid phone number format.")]
public string? PhoneNumber { get; set; }
[StringLength(100, ErrorMessage = "Full name cannot exceed 100 characters.")]
public string? FullName { get; set; }
}
}
DTOs/EmailDTO.cs

Create a class file named EmailDTO.cs within the DTOs folder of your UserService.Application project and copy and paste the following code. The EmailDTO class serves as a simple data container for operations requiring only an email address, such as initiating email confirmation or password reset requests. Isolating the email property helps ensure that only necessary data is transferred for operations involving user identification via email.

using System.ComponentModel.DataAnnotations;
namespace UserService.Application.DTOs
{
public class EmailDTO
{
[Required(ErrorMessage = "Email is required.")]
[EmailAddress(ErrorMessage = "Invalid email address format.")]
public string Email { get; set; } = null!;
}
}
DTOs/EmailConfirmationTokenResponseDTO.cs

Create a class file named EmailConfirmationTokenResponseDTO.cs within the DTOs folder of your UserService.Application project and copy and paste the following code. The EmailConfirmationTokenResponseDTO class is used to return the result of an email confirmation token generation process. It contains the user’s unique identifier (UserId) and the generated token string, allowing clients to handle email verification workflows securely and efficiently.

namespace UserService.Application.DTOs
{
public class EmailConfirmationTokenResponseDTO
{
public Guid UserId { get; set; }
public string Token { get; set; } = null!;
}
}
DTOs/ConfirmEmailDTO.cs

Create a class file named ConfirmEmailDTO.cs within the DTOs folder of your UserService.Application project and copy and paste the following code. The ConfirmEmailDTO class packages together the data needed to confirm a user’s email address. It includes both the UserId and the verification Token, which are submitted back to the API to validate and complete the email confirmation process, helping to activate the user’s account.

using System.ComponentModel.DataAnnotations;
namespace UserService.Application.DTOs
{
public class ConfirmEmailDTO
{
[Required(ErrorMessage = "User ID is required.")]
public Guid UserId { get; set; }
[Required(ErrorMessage = "Confirmation token is required.")]
public string Token { get; set; } = null!;
}
}
DTOs/LoginDTO.cs

Create a class file named LoginDTO.cs within the DTOs folder of your UserService.Application project and copy and paste the following code. The LoginDTO class represents the credentials required for user login, including an EmailOrUserName field and a Password. It is sent by clients to authenticate users, and its dual-purpose field allows flexibility in accepting either the user’s email or username as a login identifier.

using System.ComponentModel.DataAnnotations;
namespace UserService.Application.DTOs
{
public class LoginDTO
{
[Required(ErrorMessage = "Email or Username is required.")]
public string EmailOrUserName { get; set; } = null!;
[Required(ErrorMessage = "Password is required.")]
public string Password { get; set; } = null!;
[Required(ErrorMessage = "ClientId is required.")]
public string ClientId { get; set; } = null!;
}
}
DTOs/LoginResponseDTO.cs

Create a class file named LoginResponseDTO.cs within the DTOs folder of your UserService.Application project and copy and paste the following code. The LoginResponseDTO class models the response returned after a login attempt. It provides status indicators, such as Succeeded, authentication tokens (Token, RefreshToken), flags (RequiresTwoFactor), potential error messages, and remaining login attempts, supporting robust and user-friendly authentication flows.

namespace UserService.Application.DTOs
{
public class LoginResponseDTO
{
public bool Succeeded { get; set; }
public string? Token { get; set; }
public string? RefreshToken { get; set; }
public bool RequiresTwoFactor { get; set; }
public string? ErrorMessage { get; set; }
public int? RemainingAttempts { get; set; }
}
}
DTOs/RefreshTokenRequestDTO.cs

Create a class file named RefreshTokenRequestDTO.cs within the DTOs folder of your UserService.Application project and copy and paste the following code. The RefreshTokenRequestDTO class is a straightforward DTO that contains a single RefreshToken property, used when the client requests a new JWT access token by presenting a valid refresh token, thereby supporting stateless session management and secure token renewal.

using System.ComponentModel.DataAnnotations;
namespace UserService.Application.DTOs
{
public class RefreshTokenRequestDTO
{
[Required(ErrorMessage = "Refresh token is required.")]
public string RefreshToken { get; set; } = null!;
[Required(ErrorMessage = "Client ID is required.")]
public string ClientId { get; set; } = null!; // e.g., "Web", "Android", "iOS"
}
}
DTOs/RefreshTokenResponseDTO.cs

Create a class file named RefreshTokenResponseDTO.cs within the DTOs folder of your UserService.Application project and copy and paste the following code. The RefreshTokenResponseDTO class communicates the outcome of a refresh token request, containing the newly issued JWT token, a new refresh token if applicable, and an optional error message, ensuring the client has the necessary tokens to maintain authenticated sessions.

namespace UserService.Application.DTOs
{
public class RefreshTokenResponseDTO
{
public string? Token { get; set; }
public string? RefreshToken { get; set; }
public string? ErrorMessage { get; set; }
}
}
DTOs/ProfileDTO.cs

Create a class file named ProfileDTO.cs within the DTOs folder of your UserService.Application project and copy and paste the following code. The ProfileDTO class provides a snapshot of the user’s profile details, such as UserId, FullName, PhoneNumber, Email, UserName, LastLoginAt, and ProfilePhotoUrl.

namespace UserService.Application.DTOs
{
public class ProfileDTO
{
public Guid UserId { get; set; }
public string? FullName { get; set; } = null!;
public string? PhoneNumber { get; set; } = null!;
public string? Email { get; set; } = null!;
public string? UserName { get; set; } = null!;
public DateTime? LastLoginAt { get; set; }
public string? ProfilePhotoUrl { get; set; }
}
}
DTOs/UpdateProfileDTO.cs

Create a class file named UpdateProfileDTO.cs within the DTOs folder of your UserService.Application project and copy and paste the following code. The UpdateProfileDTO class carries the user’s editable profile information from the client to the server, including fields like UserId, FullName, PhoneNumber, and ProfilePhotoUrl. It supports profile update scenarios, allowing users to modify their personal information securely.

using System.ComponentModel.DataAnnotations;
namespace UserService.Application.DTOs
{
public class UpdateProfileDTO
{
[Required(ErrorMessage = "User ID is required.")]
public Guid UserId { get; set; }
[Required(ErrorMessage = "Full name is required.")]
[StringLength(50, ErrorMessage = "Full name cannot exceed 50 characters.")]
public string FullName { get; set; } = null!;
[Required(ErrorMessage = "Phone number is required.")]
[Phone(ErrorMessage = "Invalid phone number format.")]
public string PhoneNumber { get; set; } = null!;
[Url(ErrorMessage = "Profile photo URL must be a valid URL.")]
public string? ProfilePhotoUrl { get; set; }
}
}
DTOs/ForgotPasswordResponseDTO.cs

Create a class file named ForgotPasswordResponseDTO.cs within the DTOs folder of your UserService.Application project and copy and paste the following code. The ForgotPasswordResponseDTO class delivers the output of a forgotten password request, typically containing the user ID and the password reset Token. This is sent to the client (often via email) so the user can complete the password reset process securely.

namespace UserService.Application.DTOs
{
public class ForgotPasswordResponseDTO
{
public Guid UserId { get; set; }
public string Token { get; set; } = null!;
}
}
DTOs/ResetPasswordDTO.cs

Create a class file named ResetPasswordDTO.cs within the DTOs folder of your UserService.Application project and copy and paste the following code. The ResetPasswordDTO class packages the information needed to reset a user’s password, including the user ID, the reset Token, and the New Password. It enables the password reset endpoint to validate the request and securely update the user’s password.

using System.ComponentModel.DataAnnotations;
namespace UserService.Application.DTOs
{
public class ResetPasswordDTO
{
[Required(ErrorMessage = "User ID is required.")]
public Guid UserId { get; set; }
[Required(ErrorMessage = "Token is required.")]
public string Token { get; set; } = null!;
[Required(ErrorMessage = "New password is required.")]
[StringLength(100, MinimumLength = 6, ErrorMessage = "Password must be at least 6 characters.")]
public string NewPassword { get; set; } = null!;
}
}
DTOs/ChangePasswordDTO.cs

Create a class file named ChangePasswordDTO.cs within the DTOs folder of your UserService.Application project and copy and paste the following code. The ChangePasswordDTO class represents a user-initiated password change request, containing the Current Password and the New Password. This DTO is used by authenticated users wishing to change their password from within their profile or settings page.

using System.ComponentModel.DataAnnotations;
namespace UserService.Application.DTOs
{
public class ChangePasswordDTO
{
[Required(ErrorMessage = "Current password is required.")]
public string CurrentPassword { get; set; } = null!;
[Required(ErrorMessage = "New password is required.")]
[StringLength(100, MinimumLength = 6, ErrorMessage = "New password must be at least 6 characters.")]
public string NewPassword { get; set; } = null!;
}
}
DTOs/AddressDTO.cs

Create a class file named AddressDTO.cs within the DTOs folder of your UserService.Application project and copy and paste the following code. The AddressDTO class defines the structure for user address management, containing properties such as ID, user ID, address lines, City, State, Postal Code, Country, and flags for default shipping and billing addresses. It supports the addition, update, and retrieval of user address information.

using System.ComponentModel.DataAnnotations;
namespace UserService.Application.DTOs
{
public class AddressDTO
{
public Guid? Id { get; set; }
[Required(ErrorMessage = "User ID is required.")]
public Guid userId { get; set; }
[Required(ErrorMessage = "Address Line 1 is required.")]
[StringLength(100, ErrorMessage = "Address Line 1 cannot exceed 100 characters.")]
public string AddressLine1 { get; set; } = null!;
[StringLength(100, ErrorMessage = "Address Line 2 cannot exceed 100 characters.")]
public string? AddressLine2 { get; set; }
[Required(ErrorMessage = "City is required.")]
[StringLength(50, ErrorMessage = "City cannot exceed 50 characters.")]
public string City { get; set; } = null!;
[Required(ErrorMessage = "State is required.")]
[StringLength(50, ErrorMessage = "State cannot exceed 50 characters.")]
public string State { get; set; } = null!;
[Required(ErrorMessage = "Postal code is required.")]
[StringLength(20, ErrorMessage = "Postal code cannot exceed 20 characters.")]
public string PostalCode { get; set; } = null!;
[Required(ErrorMessage = "Country is required.")]
[StringLength(50, ErrorMessage = "Country cannot exceed 50 characters.")]
public string Country { get; set; } = null!;
public bool IsDefaultShipping { get; set; }
public bool IsDefaultBilling { get; set; }
}
}
DeleteAddressDTO

Create a class file named DeleteAddressDTO.cs within the DTOs folder of your UserService.Application project and copy and paste the following code. The DeleteAddressDTO class is intended for address deletion operations, holding the relevant UserId and AddressId needed to uniquely identify and remove an address record for a specific user.

using System.ComponentModel.DataAnnotations;
namespace UserService.Application.DTOs
{
public class DeleteAddressDTO
{
[Required(ErrorMessage = "User ID is required.")]
public Guid UserId { get; set; }
[Required(ErrorMessage = "Address ID is required.")]
public Guid AddressId { get; set; }
}
}
Creating IUserService

The IUserService interface specifies the set of business operations available for user management at the application layer, including registration, login, token refresh and revocation, email confirmation, password management, profile CRUD, and address handling. It abstracts the user-related use cases, supporting clear boundaries and enabling dependency injection.

Create a class file named IUserService.cs within the Services folder of your UserService.Application project and copy and paste the following code:

using UserService.Application.DTOs;
namespace UserService.Application.Services
{
public interface IUserService
{
Task<bool> RegisterAsync(RegisterDTO dto);
Task<LoginResponseDTO> LoginAsync(LoginDTO dto, string ipAddress, string userAgent);
Task<RefreshTokenResponseDTO> RefreshTokenAsync(RefreshTokenRequestDTO dto, string ipAddress, string userAgent);
Task<bool> RevokeRefreshTokenAsync(string token, string ipAddress);
Task<EmailConfirmationTokenResponseDTO?> SendConfirmationEmailAsync(string email);
Task<bool> VerifyConfirmationEmailAsync(ConfirmEmailDTO dto);
Task<ForgotPasswordResponseDTO?> ForgotPasswordAsync(string email);
Task<bool> ResetPasswordAsync(Guid userId, string token, string newPassword);
Task<bool> ChangePasswordAsync(Guid userId, string currentPassword, string newPassword);
Task<ProfileDTO?> GetProfileAsync(Guid userId);
Task<bool> UpdateProfileAsync(UpdateProfileDTO dto);
Task<bool> AddOrUpdateAddressAsync(AddressDTO dto);
Task<IEnumerable<AddressDTO>> GetAddressesAsync(Guid userId);
Task<bool> DeleteAddressAsync(Guid userId, Guid addressId);
}
}
Creating UserService

The UserService provides the actual implementation of IUserService, orchestrating various domain and infrastructure operations, including registration, authentication, profile management, password and email confirmation workflows, and address management. It coordinates repository access, enforces business rules, generates JWT tokens, and ensures the secure and correct operation of all user workflows within the service.

Create a class file named UserService.cs within the Services folder of your UserService.Application project and copy and paste the following code:

using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using UserService.Application.DTOs;
using UserService.Domain.Entities;
using UserService.Domain.Repositories;
namespace UserService.Application.Services
{
public class UserService : IUserService
{
private readonly IUserRepository _userRepository;
private readonly IConfiguration _configuration;
public UserService(IUserRepository userRepository, IConfiguration configuration)
{
_userRepository = userRepository;
_configuration = configuration;
}
public async Task<bool> RegisterAsync(RegisterDTO dto)
{
if (await _userRepository.FindByEmailAsync(dto.Email) != null) 
return false;
if (await _userRepository.FindByUserNameAsync(dto.UserName) != null) 
return false;
var user = new User
{
Id = Guid.NewGuid(),
UserName = dto.UserName,
Email = dto.Email,
PhoneNumber = dto.PhoneNumber,
FullName = dto.FullName,
CreatedAt = DateTime.UtcNow,
IsActive = true,
IsEmailConfirmed = false
};
var created = await _userRepository.CreateUserAsync(user, dto.Password);
if (!created) 
return false;
await _userRepository.AddUserToRoleAsync(user, "Customer");
return true;
}
public async Task<LoginResponseDTO> LoginAsync(LoginDTO dto, string ipAddress, string userAgent)
{
var response = new LoginResponseDTO();
// Validate Client
if (!await _userRepository.IsValidClientAsync(dto.ClientId))
{
response.ErrorMessage = "Invalid client ID.";
return response;
}
// Get user by email or username
var user = dto.EmailOrUserName.Contains("@")
? await _userRepository.FindByEmailAsync(dto.EmailOrUserName)
: await _userRepository.FindByUserNameAsync(dto.EmailOrUserName);
if (user == null)
{
response.ErrorMessage = "Invalid username or password.";
return response;
}
// Check lockout info
if (await _userRepository.IsLockedOutAsync(user))
{
var lockoutEnd = await _userRepository.GetLockoutEndDateAsync(user);
if (lockoutEnd.HasValue && lockoutEnd > DateTime.UtcNow)
{
var timeLeft = lockoutEnd.Value - DateTime.UtcNow;
response.ErrorMessage = $"Account is locked. Try again after {timeLeft.Minutes} minute(s) and {timeLeft.Seconds} second(s).";
response.RemainingAttempts = 0;
return response;
}
else
{
await _userRepository.ResetAccessFailedCountAsync(user);
}
}
if (!user.IsEmailConfirmed)
{
response.ErrorMessage = "Email not confirmed. Please verify your email.";
return response;
}
// Validate password
var passwordValid = await _userRepository.CheckPasswordAsync(user, dto.Password);
if (!passwordValid)
{
await _userRepository.IncrementAccessFailedCountAsync(user);
if (await _userRepository.IsLockedOutAsync(user))
{
response.ErrorMessage = "Account locked due to multiple failed login attempts.";
response.RemainingAttempts = 0;
return response;
}
var maxAttempts = await _userRepository.GetMaxFailedAccessAttemptsAsync();
var failedCount = await _userRepository.GetAccessFailedCountAsync(user);
var attemptsLeft = maxAttempts - failedCount;
response.ErrorMessage = "Invalid username or password.";
response.RemainingAttempts = attemptsLeft > 0 ? attemptsLeft : 0;
return response;
}
await _userRepository.ResetAccessFailedCountAsync(user);
if (await _userRepository.IsTwoFactorEnabledAsync(user))
{
response.RequiresTwoFactor = true;
return response;
}
await _userRepository.UpdateLastLoginAsync(user, DateTime.UtcNow);
var roles = await _userRepository.GetUserRolesAsync(user);
response.Token = GenerateJwtToken(user, roles, dto.ClientId);
response.RefreshToken = await _userRepository.GenerateAndStoreRefreshTokenAsync(user.Id,  dto.ClientId, userAgent, ipAddress);
return response;
}
public async Task<EmailConfirmationTokenResponseDTO?> SendConfirmationEmailAsync(string email)
{
EmailConfirmationTokenResponseDTO? emailConfirmationTokenResponseDTO = null;
var user = await _userRepository.FindByEmailAsync(email);
if (user == null)
return null;
var token = await _userRepository.GenerateEmailConfirmationTokenAsync(user);
if (token != null)
{
emailConfirmationTokenResponseDTO = new EmailConfirmationTokenResponseDTO()
{
UserId = user.Id,
Token = token
};
}
return emailConfirmationTokenResponseDTO;
}
public async Task<bool> VerifyConfirmationEmailAsync(ConfirmEmailDTO dto)
{
var user = await _userRepository.FindByIdAsync(dto.UserId);
if (user == null)
return false;
var result = await _userRepository.VerifyConfirmaionEmailAsync(user, dto.Token);
if (result)
{
user.IsActive = true;
await _userRepository.UpdateUserAsync(user);
}
return result;
}
public async Task<RefreshTokenResponseDTO> RefreshTokenAsync(RefreshTokenRequestDTO dto, string ipAddress, string userAgent)
{
var response = new RefreshTokenResponseDTO();
// Validate Client
if (!await _userRepository.IsValidClientAsync(dto.ClientId))
{
response.ErrorMessage = "Invalid client ID.";
return response;
}
var refreshTokenEntity = await _userRepository.GetRefreshTokenAsync(dto.RefreshToken);
if (refreshTokenEntity == null || !refreshTokenEntity.IsActive)
{
response.ErrorMessage = "Invalid or expired refresh token.";
return response;
}
// Revoke the old refresh token and generate a new one
var newRefreshToken = await _userRepository.GenerateAndStoreRefreshTokenAsync(refreshTokenEntity.UserId, dto.ClientId, userAgent, ipAddress);
var user = await _userRepository.FindByIdAsync(refreshTokenEntity.UserId);
if (user == null)
{
response.ErrorMessage = "User not found.";
return response;
}
var roles = await _userRepository.GetUserRolesAsync(user);
response.Token = GenerateJwtToken(user, roles, dto.ClientId);
response.RefreshToken = newRefreshToken;
return response;
}
public async Task<bool> RevokeRefreshTokenAsync(string token, string ipAddress)
{
var refreshToken = await _userRepository.GetRefreshTokenAsync(token);
if (refreshToken == null || !refreshToken.IsActive)
return false;
await _userRepository.RevokeRefreshTokenAsync(refreshToken, ipAddress);
return true;
}
public async Task<ForgotPasswordResponseDTO?> ForgotPasswordAsync(string email)
{
ForgotPasswordResponseDTO? forgotPasswordResponseDTO = null;
var user = await _userRepository.FindByEmailAsync(email);
if (user == null) 
return null;
var token = await _userRepository.GeneratePasswordResetTokenAsync(user);
if (token != null)
{
forgotPasswordResponseDTO = new ForgotPasswordResponseDTO()
{
UserId = user.Id,
Token = token
};
}
return forgotPasswordResponseDTO;
}
public async Task<bool> ChangePasswordAsync(Guid userId, string currentPassword, string newPassword)
{
var user = await _userRepository.FindByIdAsync(userId);
if (user == null) 
return false;
return await _userRepository.ChangePasswordAsync(user, currentPassword, newPassword);
}
public async Task<bool> ResetPasswordAsync(Guid userId, string token, string newPassword)
{
var user = await _userRepository.FindByIdAsync(userId);
if (user == null) return false;
return await _userRepository.ResetPasswordAsync(user, token, newPassword);
}
public async Task<ProfileDTO?> GetProfileAsync(Guid userId)
{
var user = await _userRepository.FindByIdAsync(userId);
if (user == null) return null;
return new ProfileDTO
{
UserId = user.Id,
FullName = user.FullName,
PhoneNumber = user.PhoneNumber,
ProfilePhotoUrl = user.ProfilePhotoUrl,
Email = user.Email,
LastLoginAt = user.LastLoginAt,
UserName = user.UserName
};
}
public async Task<bool> UpdateProfileAsync(UpdateProfileDTO dto)
{
var user = await _userRepository.FindByIdAsync(dto.UserId);
if (user == null) 
return false;
user.FullName = dto.FullName;
user.PhoneNumber = dto.PhoneNumber;
user.ProfilePhotoUrl = dto.ProfilePhotoUrl;
return await _userRepository.UpdateUserAsync(user);
}
public async Task<IEnumerable<AddressDTO>> GetAddressesAsync(Guid userId)
{
var addresses = await _userRepository.GetAddressesByUserIdAsync(userId);
return addresses.Select(a => new AddressDTO
{
Id = a.Id,
AddressLine1 = a.AddressLine1,
AddressLine2 = a.AddressLine2,
City = a.City,
State = a.State,
PostalCode = a.PostalCode,
Country = a.Country,
IsDefaultBilling = a.IsDefaultBilling,
IsDefaultShipping = a.IsDefaultShipping
});
}
public async Task<bool> AddOrUpdateAddressAsync(AddressDTO dto)
{
var address = new Address
{
Id = dto.Id ?? Guid.NewGuid(),
UserId = dto.userId,
AddressLine1 = dto.AddressLine1,
AddressLine2 = dto.AddressLine2,
City = dto.City,
State = dto.State,
PostalCode = dto.PostalCode,
Country = dto.Country,
IsDefaultBilling = dto.IsDefaultBilling,
IsDefaultShipping = dto.IsDefaultShipping
};
return await _userRepository.AddOrUpdateAddressAsync(address);
}
public async Task<bool> DeleteAddressAsync(Guid userId, Guid addressId)
{
return await _userRepository.DeleteAddressAsync(userId, addressId);
}
private string GenerateJwtToken(User user, IList<string> roles, string clientId)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Email, user.Email ?? ""),
new Claim(ClaimTypes.Name, user.UserName ?? ""),
new Claim("client_id", clientId)
};
// Add role claims
claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role)));
// Read JWT settings from configuration
var secretKey = _configuration["JwtSettings:SecretKey"];
var issuer = _configuration["JwtSettings:Issuer"];
var expiryMinutes = Convert.ToInt32(_configuration["JwtSettings:AccessTokenExpirationMinutes"]);
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var tokenDescriptor = new JwtSecurityToken(
issuer: issuer,
claims: claims,
expires: DateTime.UtcNow.AddMinutes(expiryMinutes),
signingCredentials: creds
);
return new JwtSecurityTokenHandler().WriteToken(tokenDescriptor);
}
}
}

Implementing API Layer (UserService.API)

The API Layer exposes the User Microservice’s functionality to clients (such as web or mobile applications, or other microservices) via HTTP endpoints.

  • It consists of controllers (like UserController) that receive HTTP requests, map them to appropriate application service calls, validate input, handle authorization, and format responses using standard DTOs and result wrappers (ApiResponse<T>).
  • This layer serves as the entry point to your service, providing a secure, consistent, and well-documented interface to all user-related features while decoupling the public contract from internal logic and persistence details.

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, making it easier for clients to handle results, display messages, and react to errors or validation issues in a predictable manner.

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 (including forgot, reset, and 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)
{
try
{
var success = await _userService.AddOrUpdateAddressAsync(dto);
if (!success)
return BadRequest(ApiResponse<string>.FailResponse("Failed to add or update address."));
return Ok(ApiResponse<string>.SuccessResponse("Address saved successfully."));
}
catch (Exception ex)
{
return StatusCode(500, ApiResponse<string>.FailResponse("Error saving address.", new List<string> { ex.Message }));
}
}
[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 }));
}
}
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 required 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 the 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 in 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;
});
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
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, allowing EF Core tools to pick up the 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. Then, apply the Migration file to create the database and the required tables. Please execute these commands in the Infrastructure project, which contains the DbContext class.

Creating and Applying Database Migration

Once you execute the above commands, verify the database, and you should see the UserServiceDB database with the required tables as shown in the image below.

User Microservice with ASP.NET Core Web API

Register Endpoint
{
"UserName": "pranaykmar777@gmail.com",
"Email": "pranaykmar777@gmail.com",
"Password": "Test@12345",
"PhoneNumber": "9853977973",
"FullName": "Pranaya Rout"
}
Login Endpoint
{
"EmailOrUserName": "pranaykmar777@gmail.com",
"Password": "Test@12345",
"ClientId": "web"
}
Address Endpoint
{
"UserId": "64AFBD8E-EB82-4C53-A202-EE6BE7187DD9",
"AddressLine1": "BBSR",
"AddressLine2": "Near Central Mall",
"City": "Bengaluru",
"State": "Karnataka",
"PostalCode": "560001",
"Country": "India",
"IsDefaultBilling": true,
"IsDefaultShipping": true
}

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 a clean architecture and adherence to security best practices, forms a robust backbone for the entire microservices ecosystem, ensuring a consistent and reliable user experience.

Leave a Reply

Your email address will not be published. Required fields are marked *