diff --git a/src/EFCore.PG/Design/Internal/NpgsqlCSharpRuntimeAnnotationCodeGenerator.cs b/src/EFCore.PG/Design/Internal/NpgsqlCSharpRuntimeAnnotationCodeGenerator.cs index f8a1519c3..9d26dfce5 100644 --- a/src/EFCore.PG/Design/Internal/NpgsqlCSharpRuntimeAnnotationCodeGenerator.cs +++ b/src/EFCore.PG/Design/Internal/NpgsqlCSharpRuntimeAnnotationCodeGenerator.cs @@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore.Design.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Design.Internal; @@ -28,6 +29,109 @@ public NpgsqlCSharpRuntimeAnnotationCodeGenerator( { } + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public override bool Create( + CoreTypeMapping typeMapping, + CSharpRuntimeAnnotationCodeGeneratorParameters parameters, + ValueComparer? valueComparer = null, + ValueComparer? keyValueComparer = null, + ValueComparer? providerValueComparer = null) + { + var result = base.Create(typeMapping, parameters, valueComparer, keyValueComparer, providerValueComparer); + + var mainBuilder = parameters.MainBuilder; + + var npgsqlDbTypeBasedDefaultInstance = typeMapping switch + { + NpgsqlStringTypeMapping => NpgsqlStringTypeMapping.Default, + NpgsqlULongTypeMapping => NpgsqlULongTypeMapping.Default, + // NpgsqlMultirangeTypeMapping => NpgsqlMultirangeTypeMapping.Default, + _ => (INpgsqlTypeMapping?)null + }; + + if (npgsqlDbTypeBasedDefaultInstance is not null) + { + var npgsqlDbType = ((INpgsqlTypeMapping)typeMapping).NpgsqlDbType; + + if (npgsqlDbType != npgsqlDbTypeBasedDefaultInstance.NpgsqlDbType) + { + mainBuilder.AppendLine(";"); + + mainBuilder.Append( + $"{parameters.TargetName}.TypeMapping = (({typeMapping.GetType().Name}){parameters.TargetName}.TypeMapping).Clone(npgsqlDbType: "); + + mainBuilder + .Append(nameof(NpgsqlTypes)) + .Append(".") + .Append(nameof(NpgsqlDbType)) + .Append(".") + .Append(npgsqlDbType.ToString()); + + mainBuilder + .Append(")") + .DecrementIndent(); + } + + } + + switch (typeMapping) + { +#pragma warning disable CS0618 // NpgsqlConnection.GlobalTypeMapper is obsolete + case NpgsqlEnumTypeMapping enumTypeMapping: + if (enumTypeMapping.NameTranslator != NpgsqlConnection.GlobalTypeMapper.DefaultNameTranslator) + { + throw new NotSupportedException( + "Mapped enums are only supported in the compiled model if they use the default name translator"); + } + break; +#pragma warning restore CS0618 + + case NpgsqlRangeTypeMapping rangeTypeMapping: + { + var defaultInstance = NpgsqlRangeTypeMapping.Default; + + var npgsqlDbTypeDifferent = rangeTypeMapping.NpgsqlDbType != defaultInstance.NpgsqlDbType; + var subtypeTypeMappingIsDifferent = rangeTypeMapping.SubtypeMapping != defaultInstance.SubtypeMapping; + + if (npgsqlDbTypeDifferent || subtypeTypeMappingIsDifferent) + { + mainBuilder.AppendLine(";"); + + mainBuilder.AppendLine( + $"{parameters.TargetName}.TypeMapping = ((NpgsqlRangeTypeMapping){parameters.TargetName}.TypeMapping).Clone(") + .IncrementIndent(); + + mainBuilder + .Append("npgsqlDbType: ") + .Append(nameof(NpgsqlTypes)) + .Append(".") + .Append(nameof(NpgsqlDbType)) + .Append(".") + .Append(rangeTypeMapping.NpgsqlDbType.ToString()) + .AppendLine(","); + + mainBuilder.Append("subtypeTypeMapping: "); + + Create(rangeTypeMapping.SubtypeMapping, parameters); + + mainBuilder + .Append(")") + .DecrementIndent(); + } + + break; + } + + } + + return result; + } + /// public override void Generate(IModel model, CSharpRuntimeAnnotationCodeGeneratorParameters parameters) { diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlEnumTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlEnumTypeMapping.cs index c99dd5e91..94f1ead19 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlEnumTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlEnumTypeMapping.cs @@ -11,9 +11,6 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; /// public class NpgsqlEnumTypeMapping : RelationalTypeMapping { - private readonly ISqlGenerationHelper _sqlGenerationHelper; - private readonly INpgsqlNameTranslator _nameTranslator; - /// /// Translates the CLR member value to the PostgreSQL value label. /// @@ -25,14 +22,25 @@ public class NpgsqlEnumTypeMapping : RelationalTypeMapping /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public NpgsqlEnumTypeMapping( - string storeType, - string? storeTypeSchema, - Type enumType, - ISqlGenerationHelper sqlGenerationHelper, - INpgsqlNameTranslator? nameTranslator = null) + public static NpgsqlEnumTypeMapping Default { get; } = new(); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual INpgsqlNameTranslator NameTranslator { get; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public NpgsqlEnumTypeMapping(string storeType, Type enumType, INpgsqlNameTranslator? nameTranslator = null) : base( - sqlGenerationHelper.DelimitIdentifier(storeType, storeTypeSchema), + storeType, enumType, jsonValueReaderWriter: (JsonValueReaderWriter?)Activator.CreateInstance( typeof(JsonPgEnumReaderWriter<>).MakeGenericType(enumType))) @@ -46,8 +54,7 @@ public NpgsqlEnumTypeMapping( nameTranslator ??= NpgsqlConnection.GlobalTypeMapper.DefaultNameTranslator; #pragma warning restore CS0618 - _nameTranslator = nameTranslator; - _sqlGenerationHelper = sqlGenerationHelper; + NameTranslator = nameTranslator; _members = CreateValueMapping(enumType, nameTranslator); } @@ -59,15 +66,24 @@ public NpgsqlEnumTypeMapping( /// protected NpgsqlEnumTypeMapping( RelationalTypeMappingParameters parameters, - ISqlGenerationHelper sqlGenerationHelper, INpgsqlNameTranslator nameTranslator) : base(parameters) { - _nameTranslator = nameTranslator; - _sqlGenerationHelper = sqlGenerationHelper; + NameTranslator = nameTranslator; _members = CreateValueMapping(parameters.CoreParameters.ClrType, nameTranslator); } + // This constructor exists only to support the static Default property above, which is necessary to allow code generation for compiled + // models. The constructor creates a completely blank type mapping, which will get cloned with all the correct details. + private NpgsqlEnumTypeMapping() + : base("some_enum", typeof(int)) + { +#pragma warning disable CS0618 // NpgsqlConnection.GlobalTypeMapper is obsolete + NameTranslator = NpgsqlConnection.GlobalTypeMapper.DefaultNameTranslator; +#pragma warning restore CS0618 + _members = null!; + } + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -75,7 +91,7 @@ protected NpgsqlEnumTypeMapping( /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlEnumTypeMapping(parameters, _sqlGenerationHelper, _nameTranslator); + => new NpgsqlEnumTypeMapping(parameters, NameTranslator); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -98,12 +114,31 @@ private static Dictionary CreateValueMapping(Type enumType, INpg x => x.GetValue(null)!, x => x.GetCustomAttribute()?.PgName ?? nameTranslator.TranslateMemberName(x.Name)); - private sealed class JsonPgEnumReaderWriter : JsonValueReaderWriter + // This is public for the compiled model + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public sealed class JsonPgEnumReaderWriter : JsonValueReaderWriter where T : struct, Enum { + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// public override T FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null) => Enum.Parse(manager.CurrentReader.GetString()!); + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// public override void ToJsonTyped(Utf8JsonWriter writer, T value) => writer.WriteStringValue(value.ToString()); } diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlRangeTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlRangeTypeMapping.cs index 7b82dbcac..7687410d6 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlRangeTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlRangeTypeMapping.cs @@ -24,6 +24,14 @@ public class NpgsqlRangeTypeMapping : NpgsqlTypeMapping private ConstructorInfo? _rangeConstructor2; private ConstructorInfo? _rangeConstructor3; + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static NpgsqlRangeTypeMapping Default { get; } = new(); + // ReSharper disable once MemberCanBePrivate.Global /// /// The relational type mapping of the range's subtype. @@ -94,6 +102,25 @@ protected NpgsqlRangeTypeMapping( SubtypeMapping = subtypeMapping; } + // This constructor exists only to support the static Default property above, which is necessary to allow code generation for compiled + // models. The constructor creates a completely blank type mapping, which will get cloned with all the correct details. + private NpgsqlRangeTypeMapping() + : this("int4range", typeof(NpgsqlRange), NpgsqlDbType.IntegerRange, subtypeMapping: null!) + { + } + + /// + /// This method exists only to support the compiled model. + /// + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual NpgsqlRangeTypeMapping Clone(NpgsqlDbType npgsqlDbType, RelationalTypeMapping subtypeTypeMapping) + => new(Parameters, npgsqlDbType, subtypeTypeMapping); + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlStringTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlStringTypeMapping.cs index c751c942c..01c94af42 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlStringTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlStringTypeMapping.cs @@ -8,6 +8,14 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; /// public class NpgsqlStringTypeMapping : StringTypeMapping, INpgsqlTypeMapping { + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static new NpgsqlStringTypeMapping Default { get; } = new("text", NpgsqlDbType.Text); + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -51,6 +59,18 @@ protected NpgsqlStringTypeMapping( protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) => new NpgsqlStringTypeMapping(parameters, NpgsqlDbType); + /// + /// This method exists only to support the compiled model. + /// + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual NpgsqlStringTypeMapping Clone(NpgsqlDbType npgsqlDbType) + => new(Parameters, npgsqlDbType); + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlULongTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlULongTypeMapping.cs index 21511990c..ef52a1e20 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlULongTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlULongTypeMapping.cs @@ -10,6 +10,14 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; /// public class NpgsqlULongTypeMapping : NpgsqlTypeMapping { + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static NpgsqlULongTypeMapping Default { get; } = new("xid8", NpgsqlDbType.Xid8); + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs b/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs index 6a91bf229..bfd022647 100644 --- a/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs +++ b/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs @@ -399,7 +399,9 @@ is PropertyInfo globalEnumTypeMappingsProperty var name = components.Length > 1 ? string.Join(null, components.Skip(1)) : adoEnumMapping.PgTypeName; var mapping = new NpgsqlEnumTypeMapping( - name, schema, adoEnumMapping.EnumClrType, sqlGenerationHelper, adoEnumMapping.NameTranslator); + sqlGenerationHelper.DelimitIdentifier(name, schema), + adoEnumMapping.EnumClrType, + adoEnumMapping.NameTranslator); ClrTypeMappings[adoEnumMapping.EnumClrType] = mapping; StoreTypeMappings[mapping.StoreType] = new RelationalTypeMapping[] { mapping }; } diff --git a/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingTest.cs b/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingTest.cs index dc972ffe9..ac1701a56 100644 --- a/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingTest.cs +++ b/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingTest.cs @@ -489,12 +489,7 @@ public void ValueComparer_hstore_as_ImmutableDictionary() [Fact] public void GenerateSqlLiteral_returns_enum_literal() { - var mapping = new NpgsqlEnumTypeMapping( - "dummy_enum", - null, - typeof(DummyEnum), - new NpgsqlSqlGenerationHelper(new RelationalSqlGenerationHelperDependencies()), - new NpgsqlSnakeCaseNameTranslator()); + var mapping = new NpgsqlEnumTypeMapping("dummy_enum", typeof(DummyEnum), new NpgsqlSnakeCaseNameTranslator()); Assert.Equal("'sad'::dummy_enum", mapping.GenerateSqlLiteral(DummyEnum.Sad)); } @@ -502,12 +497,7 @@ public void GenerateSqlLiteral_returns_enum_literal() [Fact] public void GenerateSqlLiteral_returns_enum_uppercase_literal() { - var mapping = new NpgsqlEnumTypeMapping( - "DummyEnum", - null, - typeof(DummyEnum), - new NpgsqlSqlGenerationHelper(new RelationalSqlGenerationHelperDependencies()), - new NpgsqlSnakeCaseNameTranslator()); + var mapping = new NpgsqlEnumTypeMapping(@"""DummyEnum""", typeof(DummyEnum), new NpgsqlSnakeCaseNameTranslator()); Assert.Equal(@"'sad'::""DummyEnum""", mapping.GenerateSqlLiteral(DummyEnum.Sad)); }