ASP.NET Core 3.1 - User Database Service


Ken Haggerty
Created 04/24/2020 - Updated 04/24/2020 19:02

This article will describe the implementation of a database service for managing users. I will assume you have created a new ASP.NET Core 3.1 Razor Pages project and have implemented the first 4 articles of the series. See Tutorial: Get started with Razor Pages in ASP.NET Core.

Users Without Identity Project and Article Series


Access to the research project source code may be purchased on KenHaggerty.Com at Manage > Assets.

A project which implements users without Identity has been published to demo.kenhaggerty.com.

I enjoy writing these articles. It often enhances and clarifies my coding. The research project is a result of a lot of refactoring and hopefully provides logical segues for the articles. Thank you for supporting my efforts.

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

Article Tags:

EF Core

Comment Count = 0

Please log in to comment or follow.

Login Register
Follow to get web notifications when new comments are posted to this article.
Logged in users receive web notifications for new articles, topics and assets.
Web Notifications