ASP.NET Core 3.1 - User Database Service

Ken Haggerty
Created 04/24/2020 - Updated 02/24/2021 00:13

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

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 Task AddAppUserAsync(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.

Method Signature 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.

Update User Intellisense

The AddAppUserAsync() method dosen't throw a DbUpdateConcurrencyException.

Add User Intellisense

Using a service for database functions provides a single source for common queries like get user and get list of users.

Get User Intellisense
Update 02/23/2021

I added the Enhanced User Series' article links.

Article Tags:

EF Core

Comment Count = 0

Please log in to comment.

Login Register
Logged in users receive web notifications.
Web Notifications