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

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.

Ken Haggerty
Created 04/24/20
Updated 02/24/21 00:13 GMT

Log In or Reset Quota to read more.

Article Tags:

EF Core
Successfully completed. Thank you for contributing.
Processing...
Something went wrong. Please try again.
Contribute to enjoy content without advertisments.
You can contribute without registering.

Comments(0)

Loading...
Loading...

Not accepting new comments.

Submit your comment. Comments are moderated.

User Image.
DisplayedName - Member Since ?