diff --git a/doc/mapping/enum.md b/doc/mapping/enum.md index f4fbf48e5..9b8783120 100644 --- a/doc/mapping/enum.md +++ b/doc/mapping/enum.md @@ -9,9 +9,8 @@ However, the Npgsql provider also allows you to map your CLR enums to [database First, you must specify the PostgreSQL enum type on your model, just like you would with tables, sequences or other databases objects: ```c# -protected override void OnModelCreating(ModelBuilder builder) { - builder.ForNpgsqlHasEnum("Mood", new[] { "happy", "sad" }); -} +protected override void OnModelCreating(ModelBuilder builder) + => builder.ForNpgsqlHasEnum(); ``` This causes the EF Core provider to create your data enum type, `Mood`, with two labels: `happy` and `sad`. This will cause the appropriate migration to be created. @@ -22,9 +21,7 @@ Even if your database enum is created, Npgsql has to know about it, and especial ```c# static MyDbContext() -{ - NpgsqlConnection.GlobalTypeMapper.MapEnum("Mood"); -} + => NpgsqlConnection.GlobalTypeMapper.MapEnum(); ``` This code lets Npgsql know that your CLR enum type, `Mood`, should be mapped to a database enum called `Mood`. diff --git a/src/EFCore.PG.NTS/EFCore.PG.NTS.csproj b/src/EFCore.PG.NTS/EFCore.PG.NTS.csproj index a1550ebb1..422630251 100644 --- a/src/EFCore.PG.NTS/EFCore.PG.NTS.csproj +++ b/src/EFCore.PG.NTS/EFCore.PG.NTS.csproj @@ -27,6 +27,6 @@ - + diff --git a/src/EFCore.PG.NodaTime/EFCore.PG.NodaTime.csproj b/src/EFCore.PG.NodaTime/EFCore.PG.NodaTime.csproj index 86070c18c..6e3969d79 100644 --- a/src/EFCore.PG.NodaTime/EFCore.PG.NodaTime.csproj +++ b/src/EFCore.PG.NodaTime/EFCore.PG.NodaTime.csproj @@ -26,7 +26,7 @@ - + diff --git a/src/EFCore.PG/EFCore.PG.csproj b/src/EFCore.PG/EFCore.PG.csproj index e1aef9c36..2e81f6221 100644 --- a/src/EFCore.PG/EFCore.PG.csproj +++ b/src/EFCore.PG/EFCore.PG.csproj @@ -25,7 +25,7 @@ - + diff --git a/src/EFCore.PG/Extensions/NpgsqlModelBuilderExtensions.cs b/src/EFCore.PG/Extensions/NpgsqlModelBuilderExtensions.cs index f380a2e02..dfbe59533 100644 --- a/src/EFCore.PG/Extensions/NpgsqlModelBuilderExtensions.cs +++ b/src/EFCore.PG/Extensions/NpgsqlModelBuilderExtensions.cs @@ -1,24 +1,58 @@ -using JetBrains.Annotations; +#region License + +// The PostgreSQL License +// +// Copyright (C) 2016 The Npgsql Development Team +// +// Permission to use, copy, modify, and distribute this software and its +// documentation for any purpose, without fee, and without a written +// agreement is hereby granted, provided that the above copyright notice +// and this paragraph and the following two paragraphs appear in all copies. +// +// IN NO EVENT SHALL THE NPGSQL DEVELOPMENT TEAM BE LIABLE TO ANY PARTY +// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, +// INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS +// DOCUMENTATION, EVEN IF THE NPGSQL DEVELOPMENT TEAM HAS BEEN ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +// +// THE NPGSQL DEVELOPMENT TEAM SPECIFICALLY DISCLAIMS ANY WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +// ON AN "AS IS" BASIS, AND THE NPGSQL DEVELOPMENT TEAM HAS NO OBLIGATIONS +// TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + +#endregion + +using System; +using System.Linq; +using System.Reflection; +using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Metadata; +using Npgsql; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; using Npgsql.EntityFrameworkCore.PostgreSQL.Utilities; +using Npgsql.NameTranslation; +using NpgsqlTypes; // ReSharper disable once CheckNamespace namespace Microsoft.EntityFrameworkCore { /// - /// Npgsql specific extension methods for . + /// Npgsql specific extension methods for . /// + [PublicAPI] public static class NpgsqlModelBuilderExtensions { + #region Sequences + /// - /// Configures the model to use a sequence-based hi-lo pattern to generate values for properties - /// marked as , when targeting PostgreSQL. + /// Configures the model to use a sequence-based hi-lo pattern to generate values for properties + /// marked as , when targeting PostgreSQL. /// - /// The model builder. - /// The name of the sequence. - /// The schema of the sequence. - /// The same builder instance so that multiple calls can be chained. + /// The model builder. + /// The name of the sequence. + /// The schema of the sequence. + /// The same builder instance so that multiple calls can be chained. public static ModelBuilder ForNpgsqlUseSequenceHiLo( [NotNull] this ModelBuilder modelBuilder, [CanBeNull] string name = null, @@ -45,12 +79,12 @@ public static ModelBuilder ForNpgsqlUseSequenceHiLo( } /// - /// Configures the model to use the PostgreSQL SERIAL feature to generate values for properties - /// marked as , when targeting PostgreSQL. This is the default - /// behavior when targeting PostgreSQL. + /// Configures the model to use the PostgreSQL SERIAL feature to generate values for properties + /// marked as , when targeting PostgreSQL. This is the default + /// behavior when targeting PostgreSQL. /// - /// The model builder. - /// The same builder instance so that multiple calls can be chained. + /// The model builder. + /// The same builder instance so that multiple calls can be chained. public static ModelBuilder ForNpgsqlUseSerialColumns( [NotNull] this ModelBuilder modelBuilder) { @@ -65,6 +99,8 @@ public static ModelBuilder ForNpgsqlUseSerialColumns( return modelBuilder; } + #endregion + #region Identity /// @@ -134,6 +170,8 @@ public static ModelBuilder ForNpgsqlUseIdentityColumns( #endregion Identity + #region Extensions + public static ModelBuilder HasPostgresExtension( [NotNull] this ModelBuilder modelBuilder, [NotNull] string name) @@ -145,6 +183,25 @@ public static ModelBuilder HasPostgresExtension( return modelBuilder; } + #endregion + + #region Enums + + /// + /// Registers a user-defined enum type in the model. + /// + /// The model builder in which to create the enum type. + /// The schema in which to create the enum type. + /// The name of the enum type to create. + /// The enum label values. + /// + /// The updated . + /// + /// + /// See: https://www.postgresql.org/docs/current/static/datatype-enum.html + /// + /// builder + [NotNull] public static ModelBuilder ForNpgsqlHasEnum( [NotNull] this ModelBuilder modelBuilder, [CanBeNull] string schema, @@ -159,12 +216,64 @@ public static ModelBuilder ForNpgsqlHasEnum( return modelBuilder; } + /// + /// Registers a user-defined enum type in the model. + /// + /// The model builder in which to create the enum type. + /// The name of the enum type to create. + /// The enum label values. + /// + /// The updated . + /// + /// + /// See: https://www.postgresql.org/docs/current/static/datatype-enum.html + /// + /// builder + [NotNull] public static ModelBuilder ForNpgsqlHasEnum( [NotNull] this ModelBuilder modelBuilder, [NotNull] string name, [NotNull] string[] labels) => modelBuilder.ForNpgsqlHasEnum(null, name, labels); + /// + /// Registers a user-defined enum type in the model. + /// + /// The model builder in which to create the enum type. + /// The schema in which to create the enum type. + /// The name of the enum type to create. + /// + /// The translator for name and label inference. + /// Defaults to . + /// + /// + /// The updated . + /// + /// + /// See: https://www.postgresql.org/docs/current/static/datatype-enum.html + /// + /// builder + [NotNull] + public static ModelBuilder ForNpgsqlHasEnum( + [NotNull] this ModelBuilder modelBuilder, + [CanBeNull] string schema = null, + [CanBeNull] string name = null, + [CanBeNull] INpgsqlNameTranslator nameTranslator = null) + where TEnum : struct, Enum + { + if (nameTranslator == null) + nameTranslator = NpgsqlConnection.GlobalTypeMapper.DefaultNameTranslator; + + return modelBuilder.ForNpgsqlHasEnum( + schema, + name ?? GetTypePgName(nameTranslator), + GetMemberPgNames(nameTranslator)); + } + + #endregion + + #region Templates + public static ModelBuilder HasDatabaseTemplate( [NotNull] this ModelBuilder modelBuilder, [NotNull] string templateDatabaseName) @@ -176,6 +285,10 @@ public static ModelBuilder HasDatabaseTemplate( return modelBuilder; } + #endregion + + #region Ranges + /// /// Registers a user-defined range type in the model. /// @@ -238,6 +351,10 @@ public static ModelBuilder ForNpgsqlHasRange( [NotNull] string subtype) => ForNpgsqlHasRange(modelBuilder, null, name, subtype); + #endregion + + #region Tablespaces + public static ModelBuilder ForNpgsqlUseTablespace( [NotNull] this ModelBuilder modelBuilder, [NotNull] string tablespace) @@ -248,5 +365,27 @@ public static ModelBuilder ForNpgsqlUseTablespace( modelBuilder.Model.Npgsql().Tablespace = tablespace; return modelBuilder; } + + #endregion + + #region Helpers + + // See: https://github.com/npgsql/npgsql/blob/dev/src/Npgsql/TypeMapping/TypeMapperBase.cs#L132-L138 + [NotNull] + static string GetTypePgName([NotNull] INpgsqlNameTranslator nameTranslator) where TEnum : struct, Enum + => typeof(TEnum).GetCustomAttribute()?.PgName ?? + nameTranslator.TranslateTypeName(typeof(TEnum).Name); + + // See: https://github.com/npgsql/npgsql/blob/dev/src/Npgsql/TypeHandlers/EnumHandler.cs#L118-L129 + [NotNull] + [ItemNotNull] + static string[] GetMemberPgNames([NotNull] INpgsqlNameTranslator nameTranslator) where TEnum : struct, Enum + => typeof(TEnum) + .GetFields(BindingFlags.Static | BindingFlags.Public) + .Select(x => x.GetCustomAttribute()?.PgName ?? + nameTranslator.TranslateMemberName(x.Name)) + .ToArray(); + + #endregion } } diff --git a/test/EFCore.PG.FunctionalTests/EFCore.PG.FunctionalTests.csproj b/test/EFCore.PG.FunctionalTests/EFCore.PG.FunctionalTests.csproj index ba1eb2cd6..d4a9745e7 100644 --- a/test/EFCore.PG.FunctionalTests/EFCore.PG.FunctionalTests.csproj +++ b/test/EFCore.PG.FunctionalTests/EFCore.PG.FunctionalTests.csproj @@ -26,5 +26,6 @@ + \ No newline at end of file diff --git a/test/EFCore.PG.FunctionalTests/Query/EnumQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/EnumQueryTest.cs index 6a140c7a8..fde2b2622 100644 --- a/test/EFCore.PG.FunctionalTests/Query/EnumQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/EnumQueryTest.cs @@ -16,9 +16,9 @@ public class EnumQueryTest : IClassFixture [Fact] public void Roundtrip() { - using (var ctx = CreateContext()) + using (var ctx = Fixture.CreateContext()) { - var x = ctx.SomeEntities.Single(e => e.Id == 1); + var x = ctx.SomeEntities.Single(e => e.Id == 2); Assert.Equal(MappedEnum.Sad, x.MappedEnum); } } @@ -26,7 +26,7 @@ public void Roundtrip() [Fact] public void Where_with_constant() { - using (var ctx = CreateContext()) + using (var ctx = Fixture.CreateContext()) { var x = ctx.SomeEntities.Single(e => e.MappedEnum == MappedEnum.Sad); Assert.Equal(MappedEnum.Sad, x.MappedEnum); @@ -37,7 +37,7 @@ public void Where_with_constant() [Fact] public void Where_with_parameter() { - using (var ctx = CreateContext()) + using (var ctx = Fixture.CreateContext()) { // ReSharper disable once ConvertToConstant.Local var sad = MappedEnum.Sad; @@ -51,7 +51,7 @@ public void Where_with_parameter() [Fact] public void Where_with_unmapped_enum_parameter_downcasts_are_implicit() { - using (var ctx = CreateContext()) + using (var ctx = Fixture.CreateContext()) { // ReSharper disable once ConvertToConstant.Local var sad = UnmappedEnum.Sad; @@ -64,7 +64,7 @@ public void Where_with_unmapped_enum_parameter_downcasts_are_implicit() [Fact] public void Where_with_unmapped_enum_parameter_downcasts_do_not_matter() { - using (var ctx = CreateContext()) + using (var ctx = Fixture.CreateContext()) { // ReSharper disable once ConvertToConstant.Local var sad = UnmappedEnum.Sad; @@ -77,7 +77,7 @@ public void Where_with_unmapped_enum_parameter_downcasts_do_not_matter() [Fact] public void Where_with_mapped_enum_parameter_downcasts_do_not_matter() { - using (var ctx = CreateContext()) + using (var ctx = Fixture.CreateContext()) { // ReSharper disable once ConvertToConstant.Local var sad = MappedEnum.Sad; @@ -90,7 +90,7 @@ public void Where_with_mapped_enum_parameter_downcasts_do_not_matter() [Fact] public void Where_with_unmapped_enum_parameter_downcast_for_int_comparison_does_matter() { - using (var ctx = CreateContext()) + using (var ctx = Fixture.CreateContext()) { // ReSharper disable once ConvertToConstant.Local var sad = UnmappedEnum.Sad; @@ -115,8 +115,6 @@ public EnumQueryTest(EnumFixture fixture) Fixture.TestSqlLoggerFactory.Clear(); } - EnumContext CreateContext() => Fixture.CreateContext(); - void AssertContainsInSql(string expected) => Assert.Contains(expected, Fixture.TestSqlLoggerFactory.Sql); @@ -126,14 +124,14 @@ void AssertDoesNotContainInSql(string expected) public class EnumContext : DbContext { + // ReSharper disable once UnusedAutoPropertyAccessor.Global public DbSet SomeEntities { get; set; } public EnumContext(DbContextOptions options) : base(options) {} protected override void OnModelCreating(ModelBuilder builder) - { - builder.ForNpgsqlHasEnum("mapped_enum", new[] { "happy", "sad" }); - } + => builder.ForNpgsqlHasEnum("mapped_enum", new[] { "happy", "sad" }) + .ForNpgsqlHasEnum(); } public class SomeEnumEntity @@ -144,23 +142,30 @@ public class SomeEnumEntity public UnmappedEnum UnmappedEnum { get; set; } + public InferredEnum InferredEnum { get; set; } + public int EnumValue { get; set; } } public enum MappedEnum { - // ReSharper disable once UnusedMember.Global Happy, Sad }; public enum UnmappedEnum { - // ReSharper disable once UnusedMember.Global Happy, Sad }; + public enum InferredEnum + { + Happy, + Sad + }; + + // ReSharper disable once ClassNeverInstantiated.Global public class EnumFixture : IDisposable { readonly DbContextOptions _options; @@ -172,6 +177,7 @@ public class EnumFixture : IDisposable public EnumFixture() { NpgsqlConnection.GlobalTypeMapper.MapEnum(); + NpgsqlConnection.GlobalTypeMapper.MapEnum(); _testStore = NpgsqlTestStore.CreateScratch(); @@ -189,12 +195,21 @@ public EnumFixture() ctx.Database.EnsureCreated(); ctx.SomeEntities - .Add( + .AddRange( new SomeEnumEntity { Id = 1, + MappedEnum = MappedEnum.Happy, + UnmappedEnum = UnmappedEnum.Happy, + InferredEnum = InferredEnum.Happy, + EnumValue = (int)MappedEnum.Happy + }, + new SomeEnumEntity + { + Id = 2, MappedEnum = MappedEnum.Sad, UnmappedEnum = UnmappedEnum.Sad, + InferredEnum = InferredEnum.Sad, EnumValue = (int)MappedEnum.Sad }); diff --git a/test/EFCore.PG.Plugins.FunctionalTests/EFCore.PG.Plugins.FunctionalTests.csproj b/test/EFCore.PG.Plugins.FunctionalTests/EFCore.PG.Plugins.FunctionalTests.csproj index 55bec3c45..25999ed56 100644 --- a/test/EFCore.PG.Plugins.FunctionalTests/EFCore.PG.Plugins.FunctionalTests.csproj +++ b/test/EFCore.PG.Plugins.FunctionalTests/EFCore.PG.Plugins.FunctionalTests.csproj @@ -18,5 +18,6 @@ + diff --git a/test/EFCore.PG.Tests/EFCore.PG.Tests.csproj b/test/EFCore.PG.Tests/EFCore.PG.Tests.csproj index a08019d54..cc030f5b9 100644 --- a/test/EFCore.PG.Tests/EFCore.PG.Tests.csproj +++ b/test/EFCore.PG.Tests/EFCore.PG.Tests.csproj @@ -23,6 +23,7 @@ +