From 9413fcbf6c72dcc93aed5bfaff5d9cf8f3d4f59d Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Sat, 18 Nov 2023 00:01:24 +0100 Subject: [PATCH] Support a few more type mappings in the compiled model Part of #2949 --- ...sqlCSharpRuntimeAnnotationCodeGenerator.cs | 133 ++++++++++++++++++ .../Internal/NpgsqlDesignTimeServices.cs | 6 +- .../Internal/Mapping/NpgsqlEnumTypeMapping.cs | 69 ++++++--- .../Mapping/NpgsqlMultirangeTypeMapping.cs | 27 ++++ .../Mapping/NpgsqlRangeTypeMapping.cs | 27 ++++ .../Mapping/NpgsqlStringTypeMapping.cs | 20 +++ .../Mapping/NpgsqlULongTypeMapping.cs | 8 ++ .../Internal/NpgsqlTypeMappingSource.cs | 4 +- 8 files changed, 275 insertions(+), 19 deletions(-) create mode 100644 src/EFCore.PG/Design/Internal/NpgsqlCSharpRuntimeAnnotationCodeGenerator.cs diff --git a/src/EFCore.PG/Design/Internal/NpgsqlCSharpRuntimeAnnotationCodeGenerator.cs b/src/EFCore.PG/Design/Internal/NpgsqlCSharpRuntimeAnnotationCodeGenerator.cs new file mode 100644 index 000000000..c2c97950c --- /dev/null +++ b/src/EFCore.PG/Design/Internal/NpgsqlCSharpRuntimeAnnotationCodeGenerator.cs @@ -0,0 +1,133 @@ +using Microsoft.EntityFrameworkCore.Design.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Design.Internal; + +#pragma warning disable EF1001 // RelationalCSharpRuntimeAnnotationCodeGenerator is pubternal + +// We override RelationalCSharpRuntimeAnnotationCodeGenerator to provide support for some of our type mappings. +// The relational support looks for a Default static property on the type mapping type, and then calls Clone() on it with any facets +// that are different from the default. +// That works well, unless the type mapping type has some additional state outside of the well-known facets (which are in +// RelationalTypeMappingParameters). For example, we have type mappings which have an 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 +/// 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 class NpgsqlCSharpRuntimeAnnotationCodeGenerator : RelationalCSharpRuntimeAnnotationCodeGenerator +{ + /// + /// 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 NpgsqlCSharpRuntimeAnnotationCodeGenerator(CSharpRuntimeAnnotationCodeGeneratorDependencies dependencies, + RelationalCSharpRuntimeAnnotationCodeGeneratorDependencies relationalDependencies) + : base(dependencies, relationalDependencies) + { + } + + /// + /// 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("); + + 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(nameof(NpgsqlTypes)) + .Append(".") + .Append(nameof(NpgsqlDbType)) + .Append(".") + .Append(rangeTypeMapping.NpgsqlDbType.ToString()) + .AppendLine(","); + + Create(rangeTypeMapping.SubtypeMapping, parameters); + + mainBuilder + .Append(")") + .DecrementIndent(); + } + + break; + } + + } + + return result; + } +} diff --git a/src/EFCore.PG/Design/Internal/NpgsqlDesignTimeServices.cs b/src/EFCore.PG/Design/Internal/NpgsqlDesignTimeServices.cs index d4b684b1b..60cbeb104 100644 --- a/src/EFCore.PG/Design/Internal/NpgsqlDesignTimeServices.cs +++ b/src/EFCore.PG/Design/Internal/NpgsqlDesignTimeServices.cs @@ -1,4 +1,5 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Scaffolding.Internal; +using Microsoft.EntityFrameworkCore.Design.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Scaffolding.Internal; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Design.Internal; @@ -21,7 +22,10 @@ public virtual void ConfigureDesignTimeServices(IServiceCollection serviceCollec Check.NotNull(serviceCollection, nameof(serviceCollection)); serviceCollection.AddEntityFrameworkNpgsql(); +#pragma warning disable EF1001 // Internal EF Core API usage. new EntityFrameworkRelationalDesignServicesBuilder(serviceCollection) + .TryAdd() +#pragma warning restore EF1001 // Internal EF Core API usage. .TryAdd() .TryAdd() .TryAdd() diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlEnumTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlEnumTypeMapping.cs index c99dd5e91..879ab4ba2 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 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/NpgsqlMultirangeTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlMultirangeTypeMapping.cs index bffde21f3..104c2ad41 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlMultirangeTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlMultirangeTypeMapping.cs @@ -28,6 +28,14 @@ public virtual NpgsqlRangeTypeMapping RangeMapping /// public virtual NpgsqlDbType NpgsqlDbType { 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 static NpgsqlMultirangeTypeMapping Default { get; } = new(); + /// /// Constructs an instance of the class. /// @@ -62,6 +70,25 @@ protected NpgsqlMultirangeTypeMapping( NpgsqlDbType = npgsqlDbType; } + // 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 NpgsqlMultirangeTypeMapping() + : this("int4multirange", typeof(List>), rangeMapping: 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 NpgsqlMultirangeTypeMapping 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/NpgsqlRangeTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlRangeTypeMapping.cs index 7b82dbcac..36a0b5793 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 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..d85bb2fa8 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 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 }; }