Back to: Microservices using ASP.NET Core Web API Tutorials
Implementing User Microservice Infrastructure Layer
The Infrastructure Layer provides the technical implementation required to make the Domain Layer’s contracts functional. The Infrastructure Layer bridges the domain logic with actual data storage and external services. This layer handles persistence, identity management, and token storage using Entity Framework Core and ASP.NET Core Identity.
Core components include:
- ApplicationUser & ApplicationRole classes that extend ASP.NET Identity and map directly to your database.
- UserDbContext, the EF Core context that configures tables, relationships, and seeds default roles and clients.
- UserRepository, a full implementation of IUserRepository, provides concrete data access and security workflows, including token generation, password validation, role checks, and address persistence.
It ensures:
- Secure user data storage with ASP.NET Identity.
- Integration with SQL Server using EF Core.
- Proper mapping between domain entities and database models.
- Implementation of business workflows like role assignments, token lifecycle, and address management.
Installing Required Packages
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.AspNetCore.Identity
- Install-Package Microsoft.EntityFrameworkCore.SqlServer
- Install-Package Microsoft.EntityFrameworkCore.Tools
First, create three 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 adds 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>. It 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 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 acts as the Entity Framework Core database context for the User Microservice, configuring the mappings for user, role, address, and refresh token tables, managing their relationships and seed data, and providing 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<Guid> AddOrUpdateAddressAsync(Address address) { var existing = await _dbContext.Addresses.FindAsync(address.Id); if (existing == null) { await _dbContext.Addresses.AddAsync(address); await _dbContext.SaveChangesAsync(); return address.Id; // New address Id } 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 existing.Id; // Existing address Id } } 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(); } public async Task<bool> IsUserExistsAsync(Guid userId) { return await _dbContext.Users.AsNoTracking().AnyAsync(u => u.Id == userId); } public async Task<Address?> GetAddressByUserIdAndAddressIdAsync(Guid userId, Guid addressId) { return await _dbContext.Addresses. AsNoTracking() .FirstOrDefaultAsync(a => a.UserId == userId && a.Id == addressId); } } }
The Infrastructure Layer is the bridge between abstract business definitions and real-world persistence mechanisms. By isolating infrastructure concerns here, the application maintains a clean architecture, allowing easy upgrades to databases, frameworks, or third-party services without disrupting the domain logic.