#30 Setting Up the User Database
Thursday, April 26, 2018
In this article we will define what an application user is and use it to further define an application database context. We will then specify a connection string for a LocalDB database and use it to add database access to our application services. Once we have access we will use Entity Framework's code first commands to define and create the tables that we require. Finally we will test everything out by creating our first user using our authenticator's regsiter functionality.
Parts
- Part 30: User Database
- Part 29: Canonical Url
- Part 28: Razor Page Handlers
- Part 27: Send Screen Finished
- Part 26: Stroke Dashoffset
- Part 25: Send Screen
- Part 24: Forgot Link Behaviour
- Part 23: Animating Controls (cont.)
- Part 22: Animating Controls
- Part 21: Hiding Control Groups
- Part 20: ModelState Errors (cont.)
- Part 19: ModelState Errors
- Part 18: Animating Info Pages (cont.)
- Part 17: Animating Info Pages
- Part 16: Keyboard Navigation
- Part 15: Accessing the DOM
- Part 14: All About the Username
- Part 13: CSRF Attacks
- Part 12: Http Requests
- Part 11: HMR
- Part 10: Color Inputs And Buttons
- Part 9: Animating Sub-Actions
- Part 8: Form Validation (cont.)
- Part 7: Form Validation
- Part 6: Form Group
- Part 5: Authenticator Validators
- Part 4: Authenticator Inputs
- Part 3: First Angular Component
- Part 2: Webpack
- Part 1: Beginning
You can view a completed example of the authenticator component here.
Small correction from the previous video
-
WebUi
-
Source
-
components
-
authenticator
- send-screen.component.ts
-
authenticator
-
components
-
Source
We are going to start this article by making a small correction to what we did in the previous one which is once again further evidence I shouldn't make last minute changes.
What we need to modify is the animateTimer
method inside our send screen class. The mistake that I made was not calling
the hide
method if the animateTimer
argument was defined (a).
send-screen.component.ts (1)
...
export class SendScreenComponent ... {
...
private animateTimer = (animateTimer?: IAnimateTimer) => {
...
animation.onfinish = () => {
...
if (loDashIsNil(animateTimer) === false) {
if (loDashIsNil(animateTimer.onTimerExpired) === false) {
animateTimer.onTimerExpired();
}
this.hide(animateTimer.onHidden);
} else {
...
}
};
}
...
}
Nuget Package Update
Before we begin we will setting up the user database we are going to take this opportunity to apply a couple of updates that are available for two of our nuget packages. In order to do this we simple need to right click on the dependencies item located at the root of our project within Visual Studio (b). At the time of the writing of this article the two packages and their associated version numbers that we are updating are shown in (c).
Sql server
Obviously from the title of the article we are going to be creating and manipulating a database. You get no extra bonus points if you have already guessed that the database that we will be using is a SQL database and our ORM is going to be Entity Framework. This means we need to have access to a SQL server instance either locally or on some database server. Of course the choice between the two is completely up to you. For the articles and videos you will see me using a local db instance. If you need/want to you can download the installation for SQL Server here. Depending on which version you choose it may or may not also come with an instance of SQL Server Management Studio. If it does not and you wish to follow what I will be doing you can download it from here.
Specifying an application user and the application database context
-
WebUi
-
Infrastructure
- ApplicationDbContext.cs
- ApplicationUser.cs
-
Infrastructure
Our first step is to define what an application user is. For now we just need to create a C# class, which you can name whatever you like, and inherit from
IdentityUser
(d). Next we just need to create the database context class which will inherit
from IdentityDbContext<T>
(e).
ApplicationUser.cs (1)
using Microsoft.AspNetCore.Identity;
namespace WebUi.Infrastructure
{
public class ApplicationUser : IdentityUser
{
}
}
ApplicationDbContext.cs (1)
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
namespace WebUi.Infrastructure
{
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
}
}
Specifying the connection string and adding the context
-
WebUi
- appsettings.Development.json
- Startup.cs
In order to be able to connect with our database we need to specify a connection string. Since we are developing our application it only makes sense to place the connection string in the development appsettings json file (f). Now depending on our particular setup your connection string may be different. If you have gone the same route as me and are using a LocalDB instance your connection string will be very similar if not identical to (f). The main difference would be if you choose to name your database differently than I am. For reference the name that I am specifying is 'aspcore-ng-webpack'. Once the connection string is specified we need to tell the server to add a database context to our services. To do this we just need to make a single entry in our startup file (g). We will also take this opportunity to specify the requirements for the information that the user submits with their registeration request.
appsettings.Development.json
{
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=aspcore-ng-webpack;Trusted_Connection=True;MultipleActiveResultSets=true"
},
...
}
Startup.cs (1)
using System;
...
using Microsoft.AspNetCore.Identity;
...
using Microsoft.EntityFrameworkCore;
...
using WebUi.Infrastructure;
namespace WebUi
{
public class Startup
{
...
public void ConfigureServices(...)
{
...
services.AddIdentity<ApplicationUser, IdentityRole>(x =>
{
x.User.AllowedUserNameCharacters =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";
x.User.RequireUniqueEmail = true;
x.Password.RequireDigit = true;
x.Password.RequiredLength = 12;
x.Password.RequireLowercase = true;
x.Password.RequireNonAlphanumeric = true;
x.Password.RequireUppercase = true;
x.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
x.Lockout.MaxFailedAccessAttempts = 10;
x.Lockout.AllowedForNewUsers = true;
x.SignIn.RequireConfirmedEmail = true;
x.SignIn.RequireConfirmedPhoneNumber = false;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.ConfigureApplicationCookie(x =>
{
x.ExpireTimeSpan = TimeSpan.FromDays(30);
x.LoginPath = "/account/login";
x.LogoutPath = "/account/logout";
});
services.AddDbContext<ApplicationDbContext>(x => x.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
}
public void Configure(...)
{
...
app.UseAuthentication();
...
}
}
}
If we rebuild our solution and refresh the browser we should be greeted with an actually pretty informative error message shown in (h).
-
WebUi
- Startup.cs
Once again we are lucky since the fix for the current error is very simple. We just need to define a constructor for our application db context class that accepts a configuration object (i).
ApplicationDbContext.cs (2)
...
using Microsoft.EntityFrameworkCore;
namespace WebUi.Infrastructure
{
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public ApplicationDbContext(DbContextOptions options)
: base(options)
{
}
}
}
Let's create a user
-
WebUi
-
Pages
-
Account
- Register.cshtml.cs
-
Account
-
Pages
With all of those changes/additions made we are now in a position to be able to create a user when someone registers with our application. So we will head over to our register page and modify the post method to create a user (j).
Register.cshtml.cs (1)
...
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
...
using WebUi.Extensions;
using WebUi.Infrastructure;
...
namespace WebUi.Pages.Account
{
public class RegisterModel : PageModel
{
private readonly UserManager<ApplicationUser> _userManager;
public RegisterModel(UserManager<ApplicationUser> userManager)
{
_userManager = userManager;
}
...
public IActionResult OnPost(...)
{
if (ModelState.IsValid == false)
{
return ModelState.BadRequest();
}
var user = new ApplicationUser{ UserName = request.Username, Email = request.Email };
var result = await _userManager.CreateAsync(user, request.Password);
if (result.Succeeded)
{
return new OkObjectResult(new
{
url = Url.HttpsPage("/Account/Register-Email-Sent")
});
}
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
return ModelState.BadRequest();
}
...
}
}
At this point you may be saying to yourself that I have forgotten an important step and I would say you are correct. If we send a register request to the server we will see an error message (k). This one is a bit cryptic but the problem we are having is we are trying to interact with a database that does not exist.
To fix this we are going to use Entity Framework's code first approach. To do this we need to create a migration and to do that
we just use the command add-migration
, in the Package Manager Console, followed by a name for
the migration which for the present case we will just call 'First' (l). Depending on your
setup you may or may not receive the error message also shown in (l).
If you do receive this error we just need to do what it is telling us and specify whether we are using the full framework or core. For us we are using Entity Framework Core so we just need to use the command shown in (m).
Once the migration completes we should see the steps that Entity Framework will take to create our database (n).
To actually create our database we just need to apply our migration by using the EntityFrameworkCore\Update-Database
command, once again in the
Package Manager Console (o).
With that done when we attempt to register again everything should complete successfully. Now to check that we in fact did create a user we will just fire up SQL Server Management Studio (SSMS) and take a look at our database. Again if you are using the local db approach, as I am, you just need to specify the server name and use Windows Authentication as shown in (p). If everything has gone as planned you should have a database created with whatever name you specified containing server tables (q).
If we check the contents of the users table we should see that a user has been created (r).
Updating the authenticator
-
WebUi
-
Source
-
components
-
authenticator
- authenticator.component.ts
- control-group.ts
- control-pair.ts
-
authenticator
-
components
-
Source
Now it's time to do a little more updating of our authenticator. In a previous video I mentioned that we need to transition our control groups to the left control in a pair if the user clicks on the reset button. To make this happen we need to first make a couple of simple changes to our control group (s) and control pair (t) classes. Next we need to make a change to the authenticator itself (u). And while we are at it I also mentioned that we need to modify how the authenticator picks the next control in regards to the email control. Currently we are assuming that if the user has focused the email control that they must be registering which is incorrect. We just need to make a small modification in the next control method to deal with the forgot password and forgot username sub-actions.
control-group.ts (1)
...
export class ControlGroup ... {
...
public showGroupFromLeft = (focus?: boolean) => {
...
animation.onfinish = () => {
if (loDashIsNil(focus) || focus) {
this.focus();
}
};
}
public showGroupFromRight = (focus?: boolean) => {
...
animation.onfinish = () => {
if (loDashIsNil(focus) || focus) {
this.focus();
}
};
}
...
}
control-pair.ts (1)
...
export class ControlPair ... {
...
public showGroup = (...) => {
...
if (group === this._left) {
group.showGroupFromLeft(focus);
...
} else {
group.showGroupFromRight(focus);
...
}
}
...
}
authenticator.component.ts (1)
...
export class AuthenticatorComponent ... {
...
private nextControl = (...) => {
switch(key) {
...
case this.controlEmailKey:
if (this.controlEmail.valid) {
if (this.isSubActionForgotPassword || this.isSubActionForgotUsername) {
this.focusButton();
} else if (this.isRegister) {
if (this.controlEmailConfirm.value === this.controlEmail.value) {
if (this.controlEmailConfirm.invalid) {
this.controlEmailConfirm.updateValueAndValidity({ onlySelf: true });
}
} else {
this._formController.resetControl(this.controlEmailConfirmKey);
}
this._controlPairCollection.showGroup(this.controlEmailConfirmGroupId);
}
} else {
...
}
break;
...
}
}
private previousControl = (...) => {
switch (key) {
...
case this.controlEmailKey:
if (this.isRegister) {
if (this.controlPassword.valid) {
this._controlPairCollection.showGroup(this.controlPasswordConfirmGroupId);
} else {
this._controlPairCollection.showGroup(this.controlPasswordGroupId);
}
}
break;
...
case this.buttonResetKey:
if (this.isLogin) {
if (this.isSubActionForgotPassword || this.isSubActionForgotUsername) {
this._controlPairCollection.focus(this.controlEmailGroupId);
} else {
this._controlPairCollection.focus(this.controlRememberMeGroupId);
}
} else {
if (this.controlEmail.valid) {
this._controlPairCollection.showGroup(this.controlEmailConfirmGroupId);
} else {
this._controlPairCollection.showGroup(this.controlEmailGroupId);
}
}
break;
}
}
...
private onClickReset = () => {
if (this.isSubActionForgotPassword === false && this.isSubActionForgotUsername === false) {
this._controlPairCollection.showGroup(this.controlPasswordGroupId, false);
this._controlPairCollection.showGroup(this.controlEmailGroupId, false);
...
}
...
}
...
private tabOrEnter = (...) => {
let preventDefault = true;
switch(key) {
...
case this.controlEmailKey:
if (event.shiftKey) {
if (this.isSubActionForgotPassword || this.isSubActionForgotUsername) {
preventDefault = false;
} else {
this.previousControl(key);
}
} else {
this.nextControl(key);
}
break;
...
}
return preventDefault;
}
}