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)