EF Core 1.1 Pluralization in Reverse Engineer

EF Core includes the ability to reverse engineer a model from an existing database – using the Scaffold-DbContext command in Visual Studio, or the dotnet ef dbcontext scaffold command for .NET CLI. For an introduction, see Getting Started with an Existing Database.

The Problem

By default, reverse engineering will take the table name and use it as the name of the entity class and the DbSet property on the context. This means that if your table names are pluralized, you end up with pluralized entity type names (which is weird).

public partial class Blogs
{
    public int BlogId { get; set; }
    public string Url { get; set; }
}

Alternatively, if your table names are singular, then your DbSet properties end up singular (which is also weird).

public partial class BloggingContext : DbContext
{
    public virtual DbSet<Blog> Blog { get; set; }
    public virtual DbSet<Post> Post { get; set; }

    ...
}

The Solution

When we built EF Core, we made an architectural decision to factor it as a set of small services that can be extended or replaced. This design extends into the EF Core tooling.

To alter the services that are used by tooling, you add a class to your project that implements IDesignTimeServices. We’re going to use this functionality to alter some of the services used by reverse engineer.

In particular, we’re going to replace the services that affect entity class and DbSet property names. We’ll use the Inflector pluralization library to perform the pluralization and singularization, but you could easily swap it out for a different library. The code listing is commented to explain each service we are overriding, and how we are altering it.

Using the Code in Your Project

To use this solution, copy the below code into the project you are reverse engineering into.

You will need to install the Inflector package. If you are targeting .NET Core, you won’t be able to install the package, but you can just drop in the single code file instead.

PM> Install-Package Inflector

You will also need the Entity Framework design package.

PM> Install-Package Microsoft.EntityFrameworkCore.Design

Warning: Internal Services In Use

In EF Core everything is public, including all the inner workings of EF that would historically have been internal. We did this so that folks that really want to mess around with internal services can do so without using reflection.

We put these services into *.internal namespaces and reserve the right to break the APIs at any point. This code listing uses a number of these services – so be aware that future versions of EF may break this code.

The Code

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Scaffolding;
using Microsoft.EntityFrameworkCore.Scaffolding.Configuration.Internal;
using Microsoft.EntityFrameworkCore.Scaffolding.Internal;
using Microsoft.EntityFrameworkCore.Scaffolding.Metadata;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System.ComponentModel.DataAnnotations.Schema;

namespace MyApplication
{
    /// <summary>
    /// If there is a class that implements IDesignTimeServices, 
    /// then the EF Tools will call it to allow custom services 
    /// to be registered.
    ///
    /// We implement this method so that we can replace some of 
    /// the services used during reverse engineer.
    /// </summary>
    public class MyDesignTimeServices : IDesignTimeServices
    {
        public void ConfigureDesignTimeServices(IServiceCollection services)
        {
            services.AddSingleton<IScaffoldingModelFactory, CustomRelationalScaffoldingModelFactory>();
            services.AddSingleton<DbContextWriter, CustomDbContextWriter>();
            services.AddSingleton<ConfigurationFactory, CustomConfigurationFactory>();
        }
    }

    /// <summary>
    /// ConfigurationFactory creates instances of configuration objects, 
    /// which hold any configuration needs to be performed using 
    /// Fluent API and/or data annotations.
    ///
    /// We override this so that we can always configure the table 
    /// name for each entity.
    /// </summary>
    public class CustomConfigurationFactory : ConfigurationFactory
    {
        public CustomConfigurationFactory(
            IRelationalAnnotationProvider extensionsProvider,
            CSharpUtilities cSharpUtilities,
            ScaffoldingUtilities scaffoldingUtilities)
            : base(
                  extensionsProvider, 
                  cSharpUtilities, 
                  scaffoldingUtilities)
        { }

        public override ModelConfiguration CreateModelConfiguration(
            IModel model,
            CustomConfiguration customConfiguration)
        {
            return new CustomModelConfiguration(
                this,
                model,
                customConfiguration,
                ExtensionsProvider,
                CSharpUtilities,
                ScaffoldingUtilities);
        }
    }

    /// <summary>
    /// ModelConfiguration reads the model and works out what 
    /// configuration needs to be performed using Fluent API 
    /// and/or data annotations.
    ///
    /// We override this because the base implementation will only 
    /// configure the table name if it is different than the entity 
    /// name. But, by default, EF will use the DbSet property name 
    /// as the table name. This works by default because the entity 
    /// and DbSet property have the same name, but we are changing that.
    /// </summary>
    public class CustomModelConfiguration : ModelConfiguration
    {
        private ConfigurationFactory _configurationFactory;

        public CustomModelConfiguration(
            ConfigurationFactory configurationFactory,
            IModel model,
            CustomConfiguration customConfiguration,
            IRelationalAnnotationProvider annotationProvider,
            CSharpUtilities cSharpUtilities,
            ScaffoldingUtilities scaffoldingUtilities)
            : base(
                  configurationFactory, 
                  model, 
                  customConfiguration, 
                  annotationProvider, 
                  cSharpUtilities, 
                  scaffoldingUtilities)
        {
            _configurationFactory = configurationFactory;
        }

        public override void AddTableNameConfiguration(EntityConfiguration entityConfiguration)
        {
            // Rather than being smart, we're just always configuring the 
            // table name for every entity.

            var entityType = entityConfiguration.EntityType;
            var delimitedTableName = CSharpUtilities.DelimitString(AnnotationProvider.For(entityType).TableName);
            var delimitedSchemaName = CSharpUtilities.DelimitString(AnnotationProvider.For(entityType).Schema);

            entityConfiguration.FluentApiConfigurations.Add(
                _configurationFactory.CreateFluentApiConfiguration(
                    true, /* <= hasAttributeEquivalent */
                    nameof(RelationalEntityTypeBuilderExtensions.ToTable),
                    delimitedTableName,
                    delimitedSchemaName));

            entityConfiguration.AttributeConfigurations.Add(
                _configurationFactory.CreateAttributeConfiguration(
                    nameof(TableAttribute),
                    delimitedTableName,
                    $"{nameof(TableAttribute.Schema)} = {delimitedSchemaName}"));
        }
    }

    /// <summary>
    /// SqlServerScaffoldingModelFactory reads the database schema 
    /// and turns it into an EF model.
    /// 
    /// We override this so that we can singularize entity type 
    /// names in the model.
    /// </summary>
    public class CustomRelationalScaffoldingModelFactory : SqlServerScaffoldingModelFactory
    {
        public CustomRelationalScaffoldingModelFactory(
            ILoggerFactory loggerFactory,
            IRelationalTypeMapper typeMapper,
            IDatabaseModelFactory databaseModelFactory,
            CandidateNamingService candidateNamingService)
            : base(
                  loggerFactory, 
                  typeMapper, 
                  databaseModelFactory, 
                  candidateNamingService)
        { }

        protected override string GetEntityTypeName(TableModel table)
        {
            // Use the base implementation to get a C# friendly name
            var name = base.GetEntityTypeName(table);

            // Singularize the name
            return Inflector.Inflector.Singularize(name) ?? name;
        }
    }

    /// <summary>
    /// DbContextWriter writes out the C# code for the context.
    /// 
    /// We override this so that we can pluralize the DbSet names.
    /// </summary>
    public class CustomDbContextWriter : DbContextWriter
    {
        public CustomDbContextWriter(
            ScaffoldingUtilities scaffoldingUtilities,
            CSharpUtilities cSharpUtilities)
            : base(scaffoldingUtilities, cSharpUtilities)
        { }

        public override string WriteCode(ModelConfiguration modelConfiguration)
        {
            // There is no good way to override the DbSet naming, as it uses 
            // an internal StringBuilder. This means we can't override 
            // AddDbSetProperties without re-implementing the entire class.
            // Therefore, we have to get the code and then do string manipulation 
            // to replace the DbSet property code

            var code = base.WriteCode(modelConfiguration);

            foreach (var entityConfig in modelConfiguration.EntityConfigurations)
            {
                var entityName = entityConfig.EntityType.Name;
                var setName = Inflector.Inflector.Pluralize(entityName) ?? entityName;

                code = code.Replace(
                    $"DbSet<{entityName}> {entityName}",
                    $"DbSet<{entityName}> {setName}");
            }

            return code;
        }
    }
}