Quantcast
Channel: Andrew Lock | .NET Escapades
Viewing all articles
Browse latest Browse all 743

Customising ASP.NET Core Identity EF Core naming conventions for PostgreSQL

$
0
0

ASP.NET Core Identity is an authentication and membership system that lets you easily add login functionality to your ASP.NET Core application. It is designed in a modular fashion, so you can use any "stores" for users and claims that you like, but out of the box it uses Entity Framework Core to store the entities in a database.

By default, EF Core uses naming conventions for the database entities that are typical for SQL Server. In this post I'll describe how to configure your ASP.NET Core Identity app to replace the database entity names with conventions that are more common to PostgreSQL.

I'm focusing on ASP.NET Core Identity here, where the entity table name mappings have already been defined, but there's actually nothing specific to ASP.NET Core Identity in this post. You can just as easily apply this post to EF Core in general, and use more PostgreSQL-friendly conventions for all your EF Core code. See here for the tl;dr code!

Moving to PostgreSql as a SQL Server aficionado

ASP.NET Core Identity can use any database provider that is supported by EF Core - some of which are provided by Microsoft, others are third-party or open source components. If you use the templates that come with the .NET CLI via dotnet new, you can choose SQL Server or SQLite by default. Personally, I've been working more and more with PostgreSQL, the powerful cross-platform, open source database.

As someone who's familiar with SQL Server, one of the biggest differences that can bite you when you start working with PostgreSQL is that table and column names are case sensitive! This certainly takes some getting used to, and, frankly is a royal pain in the arse to work with if you stick to your old habits. If a table is created with uppercase characters in the table or column name, then you have to ensure you get the case right, and wrap the identifiers in double quotes, as I'll show shortly.

This is unfortunate when you come from a SQL Server world, where camel-case is the norm for table and column names. For example, imagine you have a table called AspNetUsers, and you want to retrieve the Id, Email and EmailConfirmed fields:

Camel Case fields

To query this table in PostgreSQL, you'd have to do something like:

SELECT "Id", "Email", "EmailConfirmed" FROM "AspNetUsers"

Notice the quote marks we need? This only gets worse when you need to escape the quotes because you're calling from the command line, or defining a SQL query in a C# string, for example:

$ psql -d DatabaseWithCaseIssues -c "SELECT \"Id\", \"Email\", \"EmailConfirmed\" FROM \"AspNetUsers\" "

Clearly nobody wants to be dealing with this. Instead it's convention to use snake_case for database objects instead of CamelCase.

snake_case > CamelCase in PostreSQL

Snake case uses lowercase for all of the identifiers, and instead of using capitals to demarcate words, it uses an underscore, _. This is perfect for PostgreSQL, as it neatly avoids the case issue. If, we could rename our entity table names to asp_net_users, and the corresponding fields to id, email and email_confirmed, then we'd neatly side-step the quoting issue:

Snake Case fields

This makes the PostgreSQL queries way simpler, especially when you would otherwise need to escape the quote marks:

$ psql -d DatabaseWithCaseIssues -c "SELECT id, email, email_confirmed FROM asp_net_users"

If you're using EF Core, then theoretically all this wouldn't matter to you. The whole point is that you don't have to write SQL code yourself, and you can just let the underlying framework generate the necessary queries. If you use CamelCase names, then the EF Core PostgreSQL database provider will happily escape all the entity names for you.

Unfortunately, reality is a pesky beast. It's just a matter of time before you find yourself wanting to write some sort of custom query directly against the database to figure out what's going on. More often than not, if it comes to this, it's because there's an issue in production and you're trying to figure out what went wrong. The last thing you need at this stressful time is to be messing with casing issues!

Consequently, I like to ensure my database tables are easy to query, even if I'll be using EF Core or some other ORM 99% of the time.

EF Core conventions and ASP.NET Core Identity

ASP.NET Core Identity takes care of many aspects of the identity and membership system of your app for you. In particular, it creates and manages the application user, claim and role entities for you, as well as a variety of entities related to third-party logins:

Image of tables created by ASP.NET Core Identity

If you're using the EF Core package for ASP.NET Core Identity, these entities are added to an IdentityDbContext, and configured within the OnModelCreating method. If you're interested, you can view the source online - I've shown a partial definition below, that just includes the configuration for the Users property which represents the users of your app

public abstract class IdentityDbContext<TUser>
{
    public DbSet<TUser> Users { get; set; }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        builder.Entity<TUser>(b =>
        {
            b.HasKey(u => u.Id);
            b.HasIndex(u => u.NormalizedUserName).HasName("UserNameIndex").IsUnique();
            b.HasIndex(u => u.NormalizedEmail).HasName("EmailIndex");
            b.ToTable("AspNetUsers");
        }
        // additional configuration
    }
}

The IdentityDbContext uses the OnModelCreating method to configure the database schema. In particular, it defines the name of the user table to be "AspNetUsers" and sets the name of a number of indexes. The column names of the entities default to their C# property values, so they would also be CamelCased.

In your application, you would typically derive your own DbContext from the IdentityDbContext<>, and inherit all of the schema associated with ASP.NET Core Identity. In the example below I've done this, and specified TUser type for the application to be ApplicationUser:

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    { }
}

With the configuration above, the database schema would use all of the default values, including the table names, and would give the database schema we saw previously. Luckily, we can override these values and replace them with our snake case values instead.

Replacing specific values with snake case

As is often the case, there are multiple ways to achieve our desired behaviour of mapping to snake case properties. The simplest conceptually is to just overwrite the values specified in IdentityDbContext.OnModelCreating() with new values. The later values will be used to generate the database schema. We simply override the OnModelCreating() method, call the base method, and then replace the values with our own:

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    { }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);
       
        builder.Entity<TUser>(b =>
        {
            b.HasKey(u => u.Id);
            b.HasIndex(u => u.NormalizedUserName).HasName("user_name_index").IsUnique();
            b.HasIndex(u => u.NormalizedEmail).HasName("email_index");
            b.ToTable("asp_net_users");
        }
        // additional configuration
    }
}

Unfortunately, there's a problem with this. EF Core uses conventions to set the names for entities and properties where you don't explicitly define their schema name. In the example above, we didn't define the property names, so they will be CamelCase by default.

If we want to override these, then we need to add additional configuration for each entity property:

b.Property(b => b.EmailConfirmation).HasColumnName("email_confirmation");

Every. Single. Property.

Talk about laborious and fragile…

Clearly we need another way. Instead of trying to explicitly replace each value, we can use a different approach, which essentially creates alternative conventions based on the existing ones.

Replacing the default conventions with snake case

The ModelBuilder instance that is passed to the OnModelCreating() method contains all the details of the database schema that will be created. By default, the database object names will all be CamelCased.

By overriding the OnModelCreating method, you can loop through each table, column, foreign key and index, and replace the existing value with its snake case equivalent. The following example shows how you can do this for every entity in the EF Core model. The ToSnakCase() extension method (shown shortly) converts a camel case string to a snake case string.

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    { }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);
        
        foreach(var entity in builder.Model.GetEntityTypes())
        {
            // Replace table names
            entity.Relational().TableName = entity.Relational().TableName.ToSnakeCase();

            // Replace column names            
            foreach(var property in entity.GetProperties())
            {
                property.Relational().ColumnName = property.Relational().ColumnName.ToSnakeCase();
            }

            foreach(var key in entity.GetKeys())
            {
                key.Relational().Name = key.Relational().Name.ToSnakeCase();
            }

            foreach(var key in entity.GetForeignKeys())
            {
                key.Relational().Name = key.Relational().Name.ToSnakeCase();
            }

            foreach(var index in entity.GetIndexes())
            {
                index.Relational().Name = index.Relational().Name.ToSnakeCase();
            }
        }
    }
}

The ToSnakeCase() method is just a simple extension method that looks for a lower case letter or number, followed by a capital letter, and inserts an underscore. There's probably a better / more efficient way to achieve this, but it does the job!

public static class StringExtensions
{
    public static string ToSnakeCase(this string input)
    {
        if (string.IsNullOrEmpty(input)) { return input; }

        var startUnderscores = Regex.Match(input, @"^_+");
        return startUnderscores + Regex.Replace(input, @"([a-z0-9])([A-Z])", "$1_$2").ToLower();
    }
}

These conventions will replace all the database object names with snake case values, but there's one table that won't be modified, the actual migrations table. This is defined when you call UseNpgsql() or UseSqlServer(), and by default is called __EFMigrationsHistory. You'll rarely need to query it outside of migrations, so I won't worry about it for now.

With our new conventions in place, we can add the EF Core migrations for our snake case schema. If you're starting from one of the VS or dotnet new templates, delete the default migration files created by ASP.NET Core Identity:

  • 00000000000000_CreateIdentitySchema.cs
  • 00000000000000_CreateIdentitySchema.Designer.cs
  • ApplicationDbContextModelSnapshot.cs

and create a new set of migrations using:

$ dotnet ef migrations add SnakeCaseIdentitySchema

Finally, you can apply the migrations using

$ dotnet ef database update

After the update, you can see that the database schema has been suitably updated. We have snake case table names, as well as snake case columns (you can take my word for it on the foreign keys and indexes!)

Updated database schema

Now we have the best of both worlds - we can use EF Core for all our standard database actions, but have the option of hand crafting SQL queries without crazy amounts of ceremony.

Note, although this article focused on ASP.NET Core Identity, it is perfectly applicable to EF Core in general.

Summary

In this post, I showed how you could modify the OnModelCreating() method so that EF Core uses snake case for database objects instead of camel case. You can look through all the entities in EF Core's model, and change the table names, column names, keys, and indexes to use snake case. For more details on the default EF Core conventions, I recommend perusing the documentation!


Viewing all articles
Browse latest Browse all 743

Trending Articles