Skip to content

Commit

Permalink
Add Pluralization Hook to Reverse Engineer
Browse files Browse the repository at this point in the history
Introduces a new IPluralizer service that is used to singularize entity type names and pluralize DbSet names. The default implementation is a no-op, so this is just a hook where folks can easily plug in their own pluralizer.

I left this separate from the existing CandidateNamingService, which is responsible for cleaning up the table name into a C# friendly identifier.

Here is what it looks like for a developer to hook in their own pluralizer. Our code generation coverage is lacking, so I tested this against Northwind and verified by round tripping data.

```c#
public class MyDesignTimeServices : IDesignTimeServices
{
    public void ConfigureDesignTimeServices(IServiceCollection services)
    {
        services.AddSingleton<IPluralizer, MyPluralizer>();
    }
}

public class MyPluralizer : IPluralizer
{
    public string Pluralize(string name)
    {
        return Inflector.Inflector.Pluralize(name) ?? name;
    }

    public string Singularize(string name)
    {
        return Inflector.Inflector.Singularize(name) ?? name;
    }
}
```

Resolves #3060
  • Loading branch information
rowanmiller committed Feb 16, 2017
1 parent ac1d6ba commit 4dab5b6
Show file tree
Hide file tree
Showing 15 changed files with 271 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,7 @@ public virtual void AddTableNameConfiguration([NotNull] EntityConfiguration enti
nameof(TableAttribute.Schema) + " = " + delimitedSchemaName));
}
else if (AnnotationProvider.For(entityType).TableName != null
&& AnnotationProvider.For(entityType).TableName != entityType.DisplayName())
&& AnnotationProvider.For(entityType).TableName != entityType.Scaffolding().DbSetName)
{
var delimitedTableName =
CSharpUtilities.DelimitString(AnnotationProvider.For(entityType).TableName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ public virtual void AddDbSetProperties()
{
_sb.AppendLine("public virtual DbSet<"
+ entityConfig.EntityType.Name
+ "> " + entityConfig.EntityType.Name
+ "> " + entityConfig.EntityType.Scaffolding().DbSetName
+ " { get; set; }");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Internal;

namespace Microsoft.EntityFrameworkCore.Scaffolding.Internal
{
Expand All @@ -21,6 +23,7 @@ public static IServiceCollection AddScaffolding([NotNull] this IServiceCollectio
.AddSingleton<ReverseEngineeringGenerator>()
.AddSingleton<ScaffoldingUtilities>()
.AddSingleton<CandidateNamingService>()
.AddSingleton<IPluralizer, NullPluralizer>()
.AddSingleton<CSharpUtilities>()
.AddSingleton<ConfigurationFactory>()
.AddSingleton<DbContextWriter>()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using JetBrains.Annotations;

namespace Microsoft.EntityFrameworkCore.Infrastructure
{
/// <summary>
/// Converts identifiers to the plural and singular equivalents.
/// </summary>
public interface IPluralizer
{
/// <summary>
/// Gets the plural version of the given identifier. Returns the same
/// identifier if it is already pluralized.
/// </summary>
/// <param name="identifier"> The identifier to be pluralized. </param>
/// <returns> The pluralized identifier. </returns>
string Pluralize([CanBeNull] string identifier);

/// <summary>
/// Gets the singular version of the given identifier. Returns the same
/// identifier if it is already singularized.
/// </summary>
/// <param name="identifier"> The identifier to be singularized. </param>
/// <returns> The singularized identifier. </returns>
string Singularize([CanBeNull] string identifier);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using Microsoft.EntityFrameworkCore.Infrastructure;

namespace Microsoft.EntityFrameworkCore.Internal
{
/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public class NullPluralizer : IPluralizer
{
/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public virtual string Pluralize(string identifier)
{
return identifier;
}

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public virtual string Singularize(string identifier)
{
return identifier;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,11 @@ public static class ScaffoldingAnnotationNames
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public const string EntityTypeErrors = "EntityTypeErrors";

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public const string DbSetName = "DbSetName";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ protected ScaffoldingFullAnnotationNames(string prefix)
DependentEndNavigation = ScaffoldingAnnotationNames.DependentEndNavigation;
PrincipalEndNavigation = ScaffoldingAnnotationNames.PrincipalEndNavigation;
EntityTypeErrors = ScaffoldingAnnotationNames.EntityTypeErrors;
DbSetName = ScaffoldingAnnotationNames.DbSetName;
}

/// <summary>
Expand Down Expand Up @@ -62,5 +63,11 @@ protected ScaffoldingFullAnnotationNames(string prefix)
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public readonly string EntityTypeErrors;

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public readonly string DbSetName;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Scaffolding.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Utilities;

namespace Microsoft.EntityFrameworkCore.Scaffolding.Metadata
{
/// <summary>
/// Provides strongly typed access to scaffolding related annotations on an
/// <see cref="IEntityType"/> instance. Instances of this class are typically obtained via the
/// <see cref="ScaffoldingMetadataExtensions.Scaffolding(IEntityType)" /> extension method and it is not designed
/// to be directly constructed in your application code.
/// </summary>
public class ScaffoldingEntityTypeAnnotations : RelationalEntityTypeAnnotations
{
/// <summary>
/// Initializes a new instance of the <see cref="ScaffoldingEntityTypeAnnotations"/> class.
/// Instances of this class are typically obtained via the
/// <see cref="ScaffoldingMetadataExtensions.Scaffolding(IEntityType)" /> extension method and it is not designed
/// to be directly constructed in your application code.
/// </summary>
/// <param name="entity"> The entity type to access annotation on. </param>
public ScaffoldingEntityTypeAnnotations([NotNull] IEntityType entity)
: base(entity, ScaffoldingFullAnnotationNames.Instance)
{
}

/// <summary>
/// Gets or set the name of the <see cref="DbSet{TEntity}"/> property for this entity type.
/// </summary>
public virtual string DbSetName
{
get { return (string)Annotations.GetAnnotation(ScaffoldingFullAnnotationNames.Instance.DbSetName, null); }
[param: CanBeNull]
set
{
Annotations.SetAnnotation(
ScaffoldingFullAnnotationNames.Instance.DbSetName,
null,
Check.NullButNotEmpty(value, nameof(value)));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,8 @@ public static ScaffoldingModelAnnotations Scaffolding([NotNull] this IModel mode

public static ScaffoldingPropertyAnnotations Scaffolding([NotNull] this IProperty property)
=> new ScaffoldingPropertyAnnotations(Check.NotNull(property, nameof(property)));

public static ScaffoldingEntityTypeAnnotations Scaffolding([NotNull] this IEntityType entityType)
=> new ScaffoldingEntityTypeAnnotations(Check.NotNull(entityType, nameof(entityType)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,22 +36,26 @@ public class RelationalScaffoldingModelFactory : IScaffoldingModelFactory
private CSharpUniqueNamer<TableModel> _tableNamer;
private readonly IDatabaseModelFactory _databaseModelFactory;
private readonly HashSet<ColumnModel> _unmappedColumns = new HashSet<ColumnModel>();
private readonly IPluralizer _pluralizer;

public RelationalScaffoldingModelFactory(
[NotNull] ILoggerFactory loggerFactory,
[NotNull] IRelationalTypeMapper typeMapper,
[NotNull] IDatabaseModelFactory databaseModelFactory,
[NotNull] CandidateNamingService candidateNamingService)
[NotNull] CandidateNamingService candidateNamingService,
[NotNull] IPluralizer pluralizer)
{
Check.NotNull(loggerFactory, nameof(loggerFactory));
Check.NotNull(typeMapper, nameof(typeMapper));
Check.NotNull(databaseModelFactory, nameof(databaseModelFactory));
Check.NotNull(candidateNamingService, nameof(candidateNamingService));
Check.NotNull(pluralizer, nameof(pluralizer));

Logger = loggerFactory.CreateLogger<RelationalScaffoldingModelFactory>();
TypeMapper = typeMapper;
CandidateNamingService = candidateNamingService;
_databaseModelFactory = databaseModelFactory;
_pluralizer = pluralizer;
}

public virtual IModel Create(string connectionString, TableSelectionSet tableSelectionSet)
Expand Down Expand Up @@ -99,7 +103,10 @@ protected virtual IModel CreateFromDatabaseModel([NotNull] DatabaseModel databas
}

protected virtual string GetEntityTypeName([NotNull] TableModel table)
=> _tableNamer.GetName(Check.NotNull(table, nameof(table)));
=> _pluralizer.Singularize(_tableNamer.GetName(Check.NotNull(table, nameof(table))));

protected virtual string GetDbSetName([NotNull] TableModel table)
=> _pluralizer.Pluralize(_tableNamer.GetName(Check.NotNull(table, nameof(table))));

protected virtual string GetPropertyName([NotNull] ColumnModel column)
{
Expand Down Expand Up @@ -237,8 +244,12 @@ protected virtual EntityTypeBuilder VisitTable([NotNull] ModelBuilder modelBuild
Check.NotNull(table, nameof(table));

var entityTypeName = GetEntityTypeName(table);

var builder = modelBuilder.Entity(entityTypeName);

var dbSetName = GetDbSetName(table);
builder.Metadata.Scaffolding().DbSetName = dbSetName;

builder.ToTable(table.Name, table.SchemaName);

VisitColumns(builder, table.Columns);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ public SqlServerScaffoldingModelFactory(
[NotNull] ILoggerFactory loggerFactory,
[NotNull] IRelationalTypeMapper typeMapper,
[NotNull] IDatabaseModelFactory databaseModelFactory,
[NotNull] CandidateNamingService candidateNamingService)
: base(loggerFactory, typeMapper, databaseModelFactory, candidateNamingService)
[NotNull] CandidateNamingService candidateNamingService,
[NotNull] IPluralizer pluralizer)
: base(loggerFactory, typeMapper, databaseModelFactory, candidateNamingService, pluralizer)
{
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ public SqliteScaffoldingModelFactory(
[NotNull] ILoggerFactory loggerFactory,
[NotNull] IRelationalTypeMapper typeMapper,
[NotNull] IDatabaseModelFactory databaseModelFactory,
[NotNull] CandidateNamingService candidateNamingService)
: base(loggerFactory, typeMapper, databaseModelFactory, candidateNamingService)
[NotNull] CandidateNamingService candidateNamingService,
[NotNull] IPluralizer pluralizer)
: base(loggerFactory, typeMapper, databaseModelFactory, candidateNamingService, pluralizer)
{
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using Microsoft.EntityFrameworkCore.Internal;
using Xunit;

namespace Microsoft.EntityFrameworkCore.Relational.Design
{
public class NullPluralizerTest
{
[Fact]
public void Returns_same_name()
{
var pluralizer = new NullPluralizer();
var name = "blogs";
Assert.Equal(name, pluralizer.Pluralize(name));
Assert.Equal(name, pluralizer.Singularize(name));
}

[Fact]
public void Returns_same_name_when_null()
{
var pluralizer = new NullPluralizer();
Assert.Equal(null, pluralizer.Pluralize(null));
Assert.Equal(null, pluralizer.Singularize(null));
}
}
}
Loading

0 comments on commit 4dab5b6

Please sign in to comment.