Advertisement

#30 Setting Up the User Database

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.

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

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 {
                ...
            }
        };
    }
    ...
}
(a) We need to call the hide method if the hide method if the animateTimer argument was defined

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).

Right clicking on the dependencies item located at the root of our project within
            visual studio will bring up the option to manage our nuget packages.
(b) Right clicking on the dependencies item located at the root of our project within visual studio will bring up the option to manage our nuget packages.
At the time of the writing of this article the two packages that we are updating are shown
            above.
(c) At the time of the writing of this article the two packages that we are updating are shown above.

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

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
    {
    }
}
(d) The application user will contain all of the information that we need for an individual user of our application.

ApplicationDbContext.cs (1)

using Microsoft.AspNetCore.Identity.EntityFrameworkCore;

namespace WebUi.Infrastructure
{
    public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
    {
    }
}
(e) The application database context is the class we will use to communicate with Entity Framework.

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"
  },
  ...
}
(f) We need to specify the connection string for the database instance that we are using.

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();
            ...
        }
    }
}
(g) By adding an entry to our startup file we will have access to the database within our application. We are also specifying the requirements for the information that the user provides when registering an account.

If we rebuild our solution and refresh the browser we should be greeted with an actually pretty informative error message shown in (h).

Error message telling us that we need to modify our application db context class
            by creating a constructor that will accept a configuration object.
(h) Error message telling us that we need to modify our application db context class by creating a constructor that will accept a configuration object.
  • 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)
        {
            
        }
    }
}
(i) Adding a constructor to our application db context removes the error shown in (h).
Advertisement

Let's create a user

  • WebUi
    • Pages
      • Account
        • Register.cshtml.cs

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();
        }
        ...
    }
}
(j) Modifying our post method so that we can actually create a user.

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.

(k) We cannot open 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).

Adding our first migration by entering the command shown above in
            the Package Manager Console.
(l) Adding our first migration by entering the command shown above in the Package Manager Console.

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).

We need to specify that we are using Entity Framework Core.
(m) We need to specify that we are using Entity Framework Core.

Once the migration completes we should see the steps that Entity Framework will take to create our database (n).

The migration steps that Entity Framework will take in order to create our database.
(n) The migration steps that Entity Framework will take in order to create our database.

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).

Applying our first migration which will result in the creation of our database.
(o) Applying our first migration which will result in the creation of our database.

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).

(p) Using SQL Server Management Studio to interact with our LocalDB.
Our database has been created for us and it contains the tables and entries that were
            specified in our migration.
(q) Our database has been created for us and it contains the tables and entries that were specified in our migration.

If we check the contents of the users table we should see that a user has been created (r).

Image showing that our database does in fact contain our test user.
(r) Image showing that our database does in fact contain our test user.

Updating the authenticator

  • WebUi
    • Source
      • components
        • authenticator
          • authenticator.component.ts
          • control-group.ts
          • control-pair.ts

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();
            }
        };
    }
    ...
}
(s) Small change that will allow us to decide if the control should be focused after the animation is finished.

control-pair.ts (1)

...
export class ControlPair ... {
    ...
    public showGroup = (...) => {
        ...
        if (group === this._left) {
            group.showGroupFromLeft(focus);
            ...
        } else {
            group.showGroupFromRight(focus);
            ...
        }
    }
    ...
}
(t) Passing on the focus argument to the control group.

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;
    }
}
(u) Animating our controls to the left control when the user clicks the reset button and dealing modifying the next control method to handle with email control if we are in a sub-action.
Exciton Interactive LLC
Advertisement