Skip to content

Commit

Permalink
Support a few more type mappings in the compiled model
Browse files Browse the repository at this point in the history
Part of #2949
  • Loading branch information
roji committed Nov 17, 2023
1 parent 9792445 commit 9413fcb
Show file tree
Hide file tree
Showing 8 changed files with 275 additions and 19 deletions.
Original file line number Diff line number Diff line change
@@ -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

/// <summary>
/// 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.
/// </summary>
public class NpgsqlCSharpRuntimeAnnotationCodeGenerator : RelationalCSharpRuntimeAnnotationCodeGenerator
{
/// <summary>
/// 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.
/// </summary>
public NpgsqlCSharpRuntimeAnnotationCodeGenerator(CSharpRuntimeAnnotationCodeGeneratorDependencies dependencies,
RelationalCSharpRuntimeAnnotationCodeGeneratorDependencies relationalDependencies)
: base(dependencies, relationalDependencies)
{
}

/// <summary>
/// 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.
/// </summary>
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;
}
}
6 changes: 5 additions & 1 deletion src/EFCore.PG/Design/Internal/NpgsqlDesignTimeServices.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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<ICSharpRuntimeAnnotationCodeGenerator, NpgsqlCSharpRuntimeAnnotationCodeGenerator>()
#pragma warning restore EF1001 // Internal EF Core API usage.
.TryAdd<IAnnotationCodeGenerator, NpgsqlAnnotationCodeGenerator>()
.TryAdd<IDatabaseModelFactory, NpgsqlDatabaseModelFactory>()
.TryAdd<IProviderConfigurationCodeGenerator, NpgsqlCodeGenerator>()
Expand Down
69 changes: 52 additions & 17 deletions src/EFCore.PG/Storage/Internal/Mapping/NpgsqlEnumTypeMapping.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping;
/// </summary>
public class NpgsqlEnumTypeMapping : RelationalTypeMapping
{
private readonly ISqlGenerationHelper _sqlGenerationHelper;
private readonly INpgsqlNameTranslator _nameTranslator;

/// <summary>
/// Translates the CLR member value to the PostgreSQL value label.
/// </summary>
Expand All @@ -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.
/// </summary>
public NpgsqlEnumTypeMapping(
string storeType,
string? storeTypeSchema,
Type enumType,
ISqlGenerationHelper sqlGenerationHelper,
INpgsqlNameTranslator? nameTranslator = null)
public static NpgsqlEnumTypeMapping Default { get; } = new();

/// <summary>
/// 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.
/// </summary>
public INpgsqlNameTranslator NameTranslator { get; }

/// <summary>
/// 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.
/// </summary>
public NpgsqlEnumTypeMapping(string storeType, Type enumType, INpgsqlNameTranslator? nameTranslator = null)
: base(
sqlGenerationHelper.DelimitIdentifier(storeType, storeTypeSchema),
storeType,
enumType,
jsonValueReaderWriter: (JsonValueReaderWriter?)Activator.CreateInstance(
typeof(JsonPgEnumReaderWriter<>).MakeGenericType(enumType)))
Expand All @@ -46,8 +54,7 @@ public NpgsqlEnumTypeMapping(
nameTranslator ??= NpgsqlConnection.GlobalTypeMapper.DefaultNameTranslator;
#pragma warning restore CS0618

_nameTranslator = nameTranslator;
_sqlGenerationHelper = sqlGenerationHelper;
NameTranslator = nameTranslator;
_members = CreateValueMapping(enumType, nameTranslator);
}

Expand All @@ -59,23 +66,32 @@ public NpgsqlEnumTypeMapping(
/// </summary>
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!;
}

/// <summary>
/// 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.
/// </summary>
protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters)
=> new NpgsqlEnumTypeMapping(parameters, _sqlGenerationHelper, _nameTranslator);
=> new NpgsqlEnumTypeMapping(parameters, NameTranslator);

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand All @@ -98,12 +114,31 @@ private static Dictionary<object, string> CreateValueMapping(Type enumType, INpg
x => x.GetValue(null)!,
x => x.GetCustomAttribute<PgNameAttribute>()?.PgName ?? nameTranslator.TranslateMemberName(x.Name));

private sealed class JsonPgEnumReaderWriter<T> : JsonValueReaderWriter<T>
// This is public for the compiled model
/// <summary>
/// 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.
/// </summary>
public sealed class JsonPgEnumReaderWriter<T> : JsonValueReaderWriter<T>
where T : struct, Enum
{
/// <summary>
/// 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.
/// </summary>
public override T FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null)
=> Enum.Parse<T>(manager.CurrentReader.GetString()!);

/// <summary>
/// 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.
/// </summary>
public override void ToJsonTyped(Utf8JsonWriter writer, T value)
=> writer.WriteStringValue(value.ToString());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ public virtual NpgsqlRangeTypeMapping RangeMapping
/// </summary>
public virtual NpgsqlDbType NpgsqlDbType { get; }

/// <summary>
/// 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.
/// </summary>
public static NpgsqlMultirangeTypeMapping Default { get; } = new();

/// <summary>
/// Constructs an instance of the <see cref="NpgsqlRangeTypeMapping" /> class.
/// </summary>
Expand Down Expand Up @@ -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<NpgsqlRange<int>>), rangeMapping: null!)
{
}

/// <summary>
/// This method exists only to support the compiled model.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public NpgsqlMultirangeTypeMapping Clone(NpgsqlDbType npgsqlDbType)
=> new(Parameters, npgsqlDbType);

/// <summary>
/// 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
Expand Down
27 changes: 27 additions & 0 deletions src/EFCore.PG/Storage/Internal/Mapping/NpgsqlRangeTypeMapping.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ public class NpgsqlRangeTypeMapping : NpgsqlTypeMapping
private ConstructorInfo? _rangeConstructor2;
private ConstructorInfo? _rangeConstructor3;

/// <summary>
/// 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.
/// </summary>
public static NpgsqlRangeTypeMapping Default { get; } = new();

// ReSharper disable once MemberCanBePrivate.Global
/// <summary>
/// The relational type mapping of the range's subtype.
Expand Down Expand Up @@ -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<int>), NpgsqlDbType.IntegerRange, subtypeMapping: null!)
{
}

/// <summary>
/// This method exists only to support the compiled model.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public NpgsqlRangeTypeMapping Clone(NpgsqlDbType npgsqlDbType, RelationalTypeMapping subtypeTypeMapping)
=> new(Parameters, npgsqlDbType, subtypeTypeMapping);

/// <summary>
/// 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
Expand Down
Loading

0 comments on commit 9413fcb

Please sign in to comment.