Introduction to ASP.NET Core part 24: handling users

Introduction

In the previous post we added the EntityFramework repository elements to our demo project. The book entities are now saved in and extracted from a database. We successfully replaced the in-memory placeholder repository with one backed up by SQL Server. We implemented the EF repository so that it has a separate commit function that needs to be called to persist the changes. The service class calling on the repository has an extra step to make but this way it’s possible to call AddNew several times and then CommitChanges only once. We’ll therefore not send a query to the database after each and every call to AddNew but send a single set of instructions instead.

In this post we’ll start looking into how to handle users in .NET Core MVC. We’ll be using the user management features built into .NET Core and EF Core. User management is most often not part of the core business domain and using the well tested built-in security features can save us a lot of time. That is time that we can spend on the real business domain of the application. In addition they can help us with the login and logout process.

We’ll also try and separate the user related elements from the “real” business entities of our application. That is we’ll put the users in a separate database with its own data context and connection string.

User management has come a long way in .NET MVC. In earlier versions of ASP.NET it was almost compulsory to go with the built-in identity related security provider and its associated objects and database tables. Customisation was difficult and clumsy with AspNetUsers and your own user related tables existing side by side. We’ll see that the current framework provides a lot more flexibility and better extension points for your needs. Therefore don’t be afraid of using the built-in elements, they are not as scary as they used to be and they can be replaced and customised much easier than before.

A central user database

Why would you store the users in a separate database? There are some considerations to make:

  • In case there are multiple applications that your users can log onto then putting them in a central database can be a logical step to avoid duplicate entries
  • A separate user database can be a forward-looking step when considering Single-SignOn where your users can log in and log out from any of your applications. If they log in on one site then they have logged in to all your sites in one step
  • As mentioned above user-related objects are very rarely part of the true business layer of an application. Users are there for authentication and authorisation purposes so that the application can decide who can do what on its pages. Therefore it can be beneficial to keep the real business entities apart from user management even on the database level. The “main” database can store the real business entities and the user management database can hold the user management related tables. If you need a reference to a user from the main business database then you can use its ID which will be a unique string by default as we’ll see later on.
  • In large enterprise applications the user and security related features and considerations can be so significant that a separate team of developers is dedicated to those parts. In that case it’s even more important to keep their work separated from the main business for encapsulation. This is similar to separating various parts of an application into microservices where the user related elements form a microservice that acts relatively autonomously.
  • And last but not least we’ll also see how to work with multiple database context objects in a .NET project as a bonus exercise.

Installation and setup

I think you can guess by now where our journey starts. That’s right, we’ll need to import a NuGet package. Open project.json and add the following dependency:

“Microsoft.AspNetCore.Identity.EntityFrameworkCore”: “1.1.0”

This will import the libraries needed for handling users in SQL Server through EntityFramework. As you type “Microsoft.AspNetCore.Identity.” in project.json you’ll see that IntelliSense provides another provider: Microsoft.AspNetCore.Identity.MongoDb. So if you’re interested in working with a document-based database instead of the good old SQL Server then there’s an option to do that too. If you’ve never used MongoDb before then you might want to check it out here, here or here.

Next add a new folder to the application called UserManagement. This is where we’ll add our User class and get to know our first built-in object. It’s called IdentityUser and resides in the Microsoft.AspNetCore.Identity.EntityFrameworkCore namespace. Add the following class to the folder:

using Microsoft.AspNetCore.Identity.EntityFrameworkCore;

namespace DotNetCoreBookstore.UserManagement
{
    public class User : IdentityUser
    {
		public int YearOfBirth { get; set; }
    }
}

IdentityUser provides a wide range of predefined and standard user related properties such as Email, Id, PasswordHash, PhoneNumber, Claims, Roles etc. Think of IdentityUser as a built-in object ready to be used out of the box as your user object for login, logout, authentication and authorisation purposes. Deriving from IdentityUser is not mandatory for user management, but I think it is a good starting point nevertheless. Within the User class we can place some custom properties like YearOfBirth above. If you’re satisfied with what IdentityUser has as its properties then it’s not even necessary to create a separate User class. However, I’d say this is a good future-proof step even if the User class may stay empty in the beginning.

Next we’ll need a DB context. Similar to IdentityUser there is a built-in context specifically for user-related operations. It is the generic IdentityDbContext of T class where the type parameter is the type of the user, i.e. the above User class in our case. Add the following UserManagementDbContext class to the UserManagement folder:

using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

namespace DotNetCoreBookstore.UserManagement
{
    public class UserManagementDbContext : IdentityDbContext<User>
    {
		public UserManagementDbContext(DbContextOptions dbContextOptions) : base(dbContextOptions)
		{			
		}
    }
}

IdentityDbContext ultimately derives from DbContext just like our other DB context class BookStoreDbContext. The user type parameter in IdentityDbContext must be of type IdentityUser which our User class fulfills. Note how we did not have to declare a DbSet of type User, like we did in BookStoreDbContext for the Book entity. IdentityDbContext of User takes care of that as well.

IdentityDbContext has other type parameters:

  • The Role type: this defaults to IdentityRole which is also a built-in element. It handles roles in the application. E.g. we can allow certain operations if the logged in user is in a certain role. Derive from IdentityRole if you need to customise the built-in role handling properties just like we derived from IdentityUser for the user object.
  • The type of ID: defaults to string. We’ll see that later on that users get a unique string as ID in the database, but it is retrieved as string by the identity DB context. This ID type parameter applies to both roles and users.

The next step is to add a new connection string called UserManagement to appsettings.json. Here’s the new set of connection strings where DefaultConnection is only kept for reference:

"ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\MSSQLLocalDB;Database=_CHANGE_ME;Trusted_Connection=True;MultipleActiveResultSets=true",
    "BookStore": "Server=(localdb)\\MSSQLLocalDB;Database=BookStore;Trusted_Connection=True;MultipleActiveResultSets=true",
    "UserManagement": "Server=(localdb)\\MSSQLLocalDB;Database=UserManagement;Trusted_Connection=True;MultipleActiveResultSets=true"
  }

We can now register the new DB context in Startup.cs:

services.AddDbContext<BookStoreDbContext>(dbContextOptionsBuilder => dbContextOptionsBuilder.UseSqlServer(Configuration.GetConnectionString("BookStore")));
services.AddDbContext<UserManagementDbContext>(dbContextOptionsBuilder => dbContextOptionsBuilder.UseSqlServer(Configuration.GetConnectionString("UserManagement")));

We also need to declare that we want to use our UserManagementDbContext and User objects as user management handlers in our application. Add the following just underneath the AddDbContext calls:

services.AddIdentity<User, IdentityRole>().AddEntityFrameworkStores<UserManagementDbContext>();

AddIdentity accepts type parameters for the user and role objects, so we take our User class and the built-in IdentityRole class. We finally declare that we want to use EF and our matching UserManagementDbContext as identity and role providers.

The final step here is to declare that we want to use an identity provider in our application pipeline, i.e. we need to add a middleware. We can do that in the Configure method using the UseIdentity method:

app.UseIdentity();
app.UseMvc(routeBuilder =>
{
	//...previous code ignored
});		

Start the application and navigate to /books. You should get an exception of type InvalidOperationException:

An exception of type ‘System.InvalidOperationException’ occurred in Microsoft.EntityFrameworkCore.dll but was not handled in user code

Additional information: The DbContextOptions passed to the BookStoreDbContext constructor must be a DbContextOptions. When registering multiple DbContext types make sure that the constructor for each context type has a DbContextOptions parameter rather than a non-generic DbContextOptions parameter.

So our declarations…:

public UserManagementDbContext(DbContextOptions dbContextOptions) : base(dbContextOptions)
public BookStoreDbContext(DbContextOptions dbContextOptions) : base(dbContextOptions)

…are not enough, we need to provide the DB context as the type parameter for the DbContextOptions class as follows:

public BookStoreDbContext(DbContextOptions<BookStoreDbContext> dbContextOptions) : base(dbContextOptions)
public UserManagementDbContext(DbContextOptions<UserManagementDbContext> dbContextOptions) : base(dbContextOptions)

Run the application now and the books page and other pages should work normally like before. It’s great how both DB contexts were picked up by the runtime and we got a meaningful exception message.

We don’t yet do anything special with users in the demo application but we’ve successfully registered all the necessary elements. Every user can still view all the pages, add new books, view their details etc. We’ll continue our investigation in the next post.

View the list of MVC and Web API related posts here.

Advertisement

About Andras Nemes
I'm a .NET/Java developer living and working in Stockholm, Sweden.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Elliot Balynn's Blog

A directory of wonderful thoughts

Software Engineering

Web development

Disparate Opinions

Various tidbits

chsakell's Blog

WEB APPLICATION DEVELOPMENT TUTORIALS WITH OPEN-SOURCE PROJECTS

Once Upon a Camayoc

Bite-size insight on Cyber Security for the not too technical.

%d bloggers like this: