ASP.NET Core 3.1 - User Database Service
This article will describe the implementation of a database service for managing users. I will assume you have downloaded the ASP.NET Core 3.1 - Users Without Identity Project or created a new ASP.NET Core 3.1 Razor Pages project. See Tutorial: Get started with Razor Pages in ASP.NET Core. You should review the earlier articles of the Users Without Identity Project series.
Users Without Identity Project and Article Series
Creation Series
- ASP.NET Core 3.1 - Users Without Identity
- ASP.NET Core 3.1 - User Entity
- ASP.NET Core 3.1 - Password Hasher
- ASP.NET Core 3.1 - User Management
- ASP.NET Core 3.1 - Admin Role
- ASP.NET Core 3.1 - Cookie Validator
- ASP.NET Core 3.1 - Concurrency Conflicts
- ASP.NET Core 3.1 - Must Change Password
- ASP.NET Core 3.1 - User Database Service
- ASP.NET Core 3.1 - Rename Related Entities
2FA Series
- ASP.NET Core 3.1 - 2FA Without Identity
- ASP.NET Core 3.1 - 2FA User Tokens
- ASP.NET Core 3.1 - 2FA Cookie Schemes
- ASP.NET Core 3.1 - 2FA Authenticating
- ASP.NET Core 3.1 - 2FA Sign In Service
- ASP.NET Core 3.1 - 2FA QR Code Generator
- ASP.NET Core 3.1 - Admin 2FA Requirement
- ASP.NET Core 3.1 - 2FA Admin Override
Enhanced User Series
- ASP.NET Core 3.1 - Enhanced User Without Identity
- ASP.NET Core 3.1 - 2FA Recovery Codes
- ASP.NET Core 3.1 - Login Lockout
- ASP.NET Core 3.1 - Created And Last Login Date
- ASP.NET Core 3.1 - Security Stamp
- ASP.NET Core 3.1 - Token Service
- ASP.NET Core 3.1 - Confirmed Email Address
- ASP.NET Core 3.1 - Password Recovery
When I first designed KenHaggerty.Com, I used a different DbContext for related entities. This involved separate entity configurations for each DbContext > OnModelCreating. I also had issues with database migrations when I implemented an entity foreign key to the user. I refactored KenHaggerty.Com to use a single DbContext, but I use service classes which implement database functions for related entities.
You need to create an interface of the service's public properties and methods for dependency injection. I include the interface at the top of my class file. You can put the interface in it's own file but I find having the class and interface in the same window aids development. The service class must implement the interface.
public class UserService : IUserService
You inject the interface and implementation type into the service container at Startup.cs > ConfigureServices.
services.AddTransient<IUserService, UserService>();
The Users Without Identity Project implements trapping a concurrency conflict with the
DbUpdateConcurrencyException. The service needs to propagate exceptions to the invoker. The
UserService implements the SaveChangesAsync() in a private method where I trap and throw any
exception as System. Exception
. The public add and update methods trap and throw
the exception. The update method can throw 2 types of exceptions,
Microsoft. EntityFrameworkCore. DbUpdateConcurrencyException
and
Microsoft. EntityFrameworkCore. DbUpdateException
. I use a try catch to trap the 2
types in the update code. The add method will never throw a DbUpdateConcurrencyException.
UserService.cs > SaveChangesAsync():
private async Task<bool> SaveChangesAsync() { try { await _context.SaveChangesAsync().ConfigureAwait(false); } catch (Exception) { throw; } return true; }
UserService.cs > UpdateAppUserAsync():
public async Task<bool> UpdateAppUserAsync(AppUser appUser, byte[] rowVersion) { _context.Attach(appUser).State = EntityState.Modified; _context.Entry(appUser).OriginalValues["RowVersion"] = rowVersion; try { await SaveChangesAsync(); } catch (Exception) { throw; } return true; }
Pages.Admin.Users.Edit.cshtml.cs > OnPostAsync():
try { _ = await _userService.UpdateAppUserAsync(appUser, Input.RowVersion); } catch (DbUpdateConcurrencyException ex) { var entry = ex.Entries.Single(); var databaseEntry = entry.GetDatabaseValues(); if (databaseEntry == null) { ModelState.AddModelError(string.Empty, "Unable to save changes. The User was deleted by another user. Click Cancel."); } else { ModelState.Clear(); // required to update Input model ModelState.AddModelError(string.Empty, "The record you attempted to edit " + "was modified by another user after you got the original values. The " + "edit operation was canceled and the current values in the database " + "have been displayed. You can continue to edit then Save again. " + "Otherwise click Cancel."); var databaseValues = (AppUser)databaseEntry.ToObject(); Input = new EditUserInputModel() { Id = databaseValues.Id, LoginName = databaseValues.LoginName, MustChangePassword = databaseValues.MustChangePassword, IsAdmin = databaseValues.IsAdmin, RowVersion = databaseValues.RowVersion }; } return Page(); } catch (RetryLimitExceededException /* dex */) { // Retry Limit = 6 ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem " + "persists, see your system administrator."); return Page(); } catch (DbUpdateException) { throw new InvalidOperationException("DbUpdateException occurred creating a new user."); }
UserService.cs > AddAppUserAsync():
public async TaskAddAppUserAsync(AppUser appUser) { _context.AppUsers.Add(appUser); try { await SaveChangesAsync(); } catch (Exception) { throw; } return true; }
Pages.Admin.Users.Create.cshtml.cs > OnPostAsync():
try { _ = await _userService.AddAppUserAsync(newUser); } catch (DbUpdateException) { throw new InvalidOperationException("DbUpdateException occurred creating a user."); }
The interface provides the method signatures to intellisense.
I added a xml description to the method. This is easily done by entering 3 slashes (///) on the empty line above the method which generates a template with parameters defined. I completed the empty fields and added my exceptions to the description.
/// <summary> /// Updates the <see cref="AppUser" /> if /// <see cref="AppUser.RowVersion" /> matches /// <paramref name="rowVersion" /> asynchronous. /// </summary> /// <param name="appUser"></param> /// <param name="rowVersion"></param> /// <returns><see cref="Task{TResult}" /> for <see cref="bool" /></returns> /// <exception cref="DbUpdateConcurrencyException"></exception> /// <exception cref="DbUpdateException"></exception> public async Task<bool> UpdateAppUserAsync(AppUser appUser, byte[] rowVersion)
I thought it would provide all this information to intellisense. I didn't find joy until I moved the xml description to the interface signature.
The AddAppUserAsync()
method dosen't throw a DbUpdateConcurrencyException.
Using a service for database functions provides a single source for common queries like get user and get list of users.
Update 02/23/2021
I added the Enhanced User Series' article links.
Comments(0)