Skip to content

Commit ab0b668

Browse files
Support for non-generic Enum mapping (#4852)
Closes #3383
1 parent 61ed499 commit ab0b668

8 files changed

Lines changed: 213 additions & 2 deletions

File tree

src/Npgsql/NpgsqlDataSourceBuilder.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,17 @@ void INpgsqlTypeMapper.Reset()
291291
where TEnum : struct, Enum
292292
=> _internalBuilder.UnmapEnum<TEnum>(pgName, nameTranslator);
293293

294+
/// <inheritdoc />
295+
[RequiresDynamicCode("Calling MapEnum with a Type can require creating new generic types or methods. This may not work when AOT compiling.")]
296+
public INpgsqlTypeMapper MapEnum([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
297+
Type clrType, string? pgName = null, INpgsqlNameTranslator? nameTranslator = null)
298+
=> _internalBuilder.MapEnum(clrType, pgName, nameTranslator);
299+
300+
/// <inheritdoc />
301+
public bool UnmapEnum([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
302+
Type clrType, string? pgName = null, INpgsqlNameTranslator? nameTranslator = null)
303+
=> _internalBuilder.UnmapEnum(clrType, pgName, nameTranslator);
304+
294305
/// <inheritdoc />
295306
[RequiresDynamicCode("Mapping composite types involves serializing arbitrary types, requiring require creating new generic types or methods. This is currently unsupported with NativeAOT, vote on issue #5303 if this is important to you.")]
296307
public INpgsqlTypeMapper MapComposite<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicFields)] T>(

src/Npgsql/NpgsqlSlimDataSourceBuilder.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,20 @@ public INpgsqlNameTranslator DefaultNameTranslator
274274
where TEnum : struct, Enum
275275
=> _userTypeMapper.UnmapEnum<TEnum>(pgName, nameTranslator);
276276

277+
/// <inheritdoc />
278+
[RequiresDynamicCode("Calling MapEnum with a Type can require creating new generic types or methods. This may not work when AOT compiling.")]
279+
public INpgsqlTypeMapper MapEnum([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
280+
Type clrType, string? pgName = null, INpgsqlNameTranslator? nameTranslator = null)
281+
{
282+
_userTypeMapper.MapEnum(clrType, pgName, nameTranslator);
283+
return this;
284+
}
285+
286+
/// <inheritdoc />
287+
public bool UnmapEnum([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
288+
Type clrType, string? pgName = null, INpgsqlNameTranslator? nameTranslator = null)
289+
=> _userTypeMapper.UnmapEnum(clrType, pgName, nameTranslator);
290+
277291
/// <inheritdoc />
278292
[RequiresDynamicCode("Mapping composite types involves serializing arbitrary types, requiring require creating new generic types or methods. This is currently unsupported with NativeAOT, vote on issue #5303 if this is important to you.")]
279293
public INpgsqlTypeMapper MapComposite<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicFields)] T>(

src/Npgsql/PublicAPI.Unshipped.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ Npgsql.NpgsqlConnectionStringBuilder.ChannelBinding.set -> void
1111
Npgsql.NpgsqlBinaryImporter.WriteRow(params object?[]! values) -> void
1212
Npgsql.NpgsqlBinaryImporter.WriteRowAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken), params object?[]! values) -> System.Threading.Tasks.Task!
1313
Npgsql.NpgsqlDataSourceBuilder.AddTypeInfoResolver(Npgsql.Internal.IPgTypeInfoResolver! resolver) -> void
14+
Npgsql.NpgsqlDataSourceBuilder.MapEnum(System.Type! clrType, string? pgName = null, Npgsql.INpgsqlNameTranslator? nameTranslator = null) -> Npgsql.TypeMapping.INpgsqlTypeMapper!
1415
Npgsql.NpgsqlDataSourceBuilder.Name.get -> string?
1516
Npgsql.NpgsqlDataSourceBuilder.Name.set -> void
17+
Npgsql.NpgsqlDataSourceBuilder.UnmapEnum(System.Type! clrType, string? pgName = null, Npgsql.INpgsqlNameTranslator? nameTranslator = null) -> bool
1618
Npgsql.NpgsqlDataSourceBuilder.UseRootCertificate(System.Security.Cryptography.X509Certificates.X509Certificate2? rootCertificate) -> Npgsql.NpgsqlDataSourceBuilder!
1719
Npgsql.NpgsqlDataSourceBuilder.UseRootCertificateCallback(System.Func<System.Security.Cryptography.X509Certificates.X509Certificate2!>? rootCertificateCallback) -> Npgsql.NpgsqlDataSourceBuilder!
1820
Npgsql.NpgsqlSlimDataSourceBuilder
@@ -35,12 +37,14 @@ Npgsql.NpgsqlSlimDataSourceBuilder.EnableRecords() -> Npgsql.NpgsqlSlimDataSourc
3537
Npgsql.NpgsqlSlimDataSourceBuilder.EnableTransportSecurity() -> Npgsql.NpgsqlSlimDataSourceBuilder!
3638
Npgsql.NpgsqlSlimDataSourceBuilder.MapComposite(System.Type! clrType, string? pgName = null, Npgsql.INpgsqlNameTranslator? nameTranslator = null) -> Npgsql.TypeMapping.INpgsqlTypeMapper!
3739
Npgsql.NpgsqlSlimDataSourceBuilder.MapComposite<T>(string? pgName = null, Npgsql.INpgsqlNameTranslator? nameTranslator = null) -> Npgsql.TypeMapping.INpgsqlTypeMapper!
40+
Npgsql.NpgsqlSlimDataSourceBuilder.MapEnum(System.Type! clrType, string? pgName = null, Npgsql.INpgsqlNameTranslator? nameTranslator = null) -> Npgsql.TypeMapping.INpgsqlTypeMapper!
3841
Npgsql.NpgsqlSlimDataSourceBuilder.MapEnum<TEnum>(string? pgName = null, Npgsql.INpgsqlNameTranslator? nameTranslator = null) -> Npgsql.TypeMapping.INpgsqlTypeMapper!
3942
Npgsql.NpgsqlSlimDataSourceBuilder.Name.get -> string?
4043
Npgsql.NpgsqlSlimDataSourceBuilder.Name.set -> void
4144
Npgsql.NpgsqlSlimDataSourceBuilder.NpgsqlSlimDataSourceBuilder(string? connectionString = null) -> void
4245
Npgsql.NpgsqlSlimDataSourceBuilder.UnmapComposite(System.Type! clrType, string? pgName = null, Npgsql.INpgsqlNameTranslator? nameTranslator = null) -> bool
4346
Npgsql.NpgsqlSlimDataSourceBuilder.UnmapComposite<T>(string? pgName = null, Npgsql.INpgsqlNameTranslator? nameTranslator = null) -> bool
47+
Npgsql.NpgsqlSlimDataSourceBuilder.UnmapEnum(System.Type! clrType, string? pgName = null, Npgsql.INpgsqlNameTranslator? nameTranslator = null) -> bool
4448
Npgsql.NpgsqlSlimDataSourceBuilder.UnmapEnum<TEnum>(string? pgName = null, Npgsql.INpgsqlNameTranslator? nameTranslator = null) -> bool
4549
Npgsql.NpgsqlSlimDataSourceBuilder.UseClientCertificate(System.Security.Cryptography.X509Certificates.X509Certificate? clientCertificate) -> Npgsql.NpgsqlSlimDataSourceBuilder!
4650
Npgsql.NpgsqlSlimDataSourceBuilder.UseClientCertificates(System.Security.Cryptography.X509Certificates.X509CertificateCollection? clientCertificates) -> Npgsql.NpgsqlSlimDataSourceBuilder!
@@ -58,6 +62,8 @@ Npgsql.Replication.PhysicalReplicationConnection.StartReplication(NpgsqlTypes.Np
5862
Npgsql.Replication.PhysicalReplicationSlot.PhysicalReplicationSlot(string! slotName, NpgsqlTypes.NpgsqlLogSequenceNumber? restartLsn = null, uint? restartTimeline = null) -> void
5963
Npgsql.Replication.PhysicalReplicationSlot.RestartTimeline.get -> uint?
6064
Npgsql.TypeMapping.INpgsqlTypeMapper.AddTypeInfoResolver(Npgsql.Internal.IPgTypeInfoResolver! resolver) -> void
65+
Npgsql.TypeMapping.INpgsqlTypeMapper.MapEnum(System.Type! clrType, string? pgName = null, Npgsql.INpgsqlNameTranslator? nameTranslator = null) -> Npgsql.TypeMapping.INpgsqlTypeMapper!
66+
Npgsql.TypeMapping.INpgsqlTypeMapper.UnmapEnum(System.Type! clrType, string? pgName = null, Npgsql.INpgsqlNameTranslator? nameTranslator = null) -> bool
6167
Npgsql.TypeMapping.UserTypeMapping
6268
Npgsql.TypeMapping.UserTypeMapping.ClrType.get -> System.Type!
6369
Npgsql.TypeMapping.UserTypeMapping.PgTypeName.get -> string!

src/Npgsql/TypeMapping/GlobalTypeMapper.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,41 @@ public INpgsqlNameTranslator DefaultNameTranslator
215215
}
216216
}
217217

218+
/// <inheritdoc />
219+
[RequiresDynamicCode("Calling MapEnum with a Type can require creating new generic types or methods. This may not work when AOT compiling.")]
220+
public INpgsqlTypeMapper MapEnum([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
221+
Type clrType, string? pgName = null, INpgsqlNameTranslator? nameTranslator = null)
222+
{
223+
_lock.EnterWriteLock();
224+
try
225+
{
226+
_userTypeMapper.MapEnum(clrType, pgName, nameTranslator);
227+
ResetTypeMappingCache();
228+
return this;
229+
}
230+
finally
231+
{
232+
_lock.ExitWriteLock();
233+
}
234+
}
235+
236+
/// <inheritdoc />
237+
public bool UnmapEnum([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
238+
Type clrType, string? pgName = null, INpgsqlNameTranslator? nameTranslator = null)
239+
{
240+
_lock.EnterWriteLock();
241+
try
242+
{
243+
var result = _userTypeMapper.UnmapEnum(clrType, pgName, nameTranslator);
244+
ResetTypeMappingCache();
245+
return result;
246+
}
247+
finally
248+
{
249+
_lock.ExitWriteLock();
250+
}
251+
}
252+
218253
/// <inheritdoc />
219254
[RequiresDynamicCode("Mapping composite types involves serializing arbitrary types, requiring require creating new generic types or methods. This is currently unsupported with NativeAOT, vote on issue #5303 if this is important to you.")]
220255
public INpgsqlTypeMapper MapComposite<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicFields)] T>(string? pgName = null, INpgsqlNameTranslator? nameTranslator = null)

src/Npgsql/TypeMapping/INpgsqlTypeMapper.cs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,49 @@ public interface INpgsqlTypeMapper
6464
INpgsqlNameTranslator? nameTranslator = null)
6565
where TEnum : struct, Enum;
6666

67+
/// <summary>
68+
/// Maps a CLR enum to a PostgreSQL enum type.
69+
/// </summary>
70+
/// <remarks>
71+
/// CLR enum labels are mapped by name to PostgreSQL enum labels.
72+
/// The translation strategy can be controlled by the <paramref name="nameTranslator"/> parameter,
73+
/// which defaults to <see cref="NpgsqlSnakeCaseNameTranslator"/>.
74+
/// You can also use the <see cref="PgNameAttribute"/> on your enum fields to manually specify a PostgreSQL enum label.
75+
/// If there is a discrepancy between the .NET and database labels while an enum is read or written,
76+
/// an exception will be raised.
77+
/// </remarks>
78+
/// <param name="clrType">The .NET enum type to be mapped</param>
79+
/// <param name="pgName">
80+
/// A PostgreSQL type name for the corresponding enum type in the database.
81+
/// If null, the name translator given in <paramref name="nameTranslator"/> will be used.
82+
/// </param>
83+
/// <param name="nameTranslator">
84+
/// A component which will be used to translate CLR names (e.g. SomeClass) into database names (e.g. some_class).
85+
/// Defaults to <see cref="DefaultNameTranslator" />.
86+
/// </param>
87+
[RequiresDynamicCode("Calling MapEnum with a Type can require creating new generic types or methods. This may not work when AOT compiling.")]
88+
INpgsqlTypeMapper MapEnum(
89+
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]Type clrType,
90+
string? pgName = null,
91+
INpgsqlNameTranslator? nameTranslator = null);
92+
93+
/// <summary>
94+
/// Removes an existing enum mapping.
95+
/// </summary>
96+
/// <param name="clrType">The .NET enum type to be mapped</param>
97+
/// <param name="pgName">
98+
/// A PostgreSQL type name for the corresponding enum type in the database.
99+
/// If null, the name translator given in <paramref name="nameTranslator"/> will be used.
100+
/// </param>
101+
/// <param name="nameTranslator">
102+
/// A component which will be used to translate CLR names (e.g. SomeClass) into database names (e.g. some_class).
103+
/// Defaults to <see cref="DefaultNameTranslator" />.
104+
/// </param>
105+
bool UnmapEnum(
106+
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]Type clrType,
107+
string? pgName = null,
108+
INpgsqlNameTranslator? nameTranslator = null);
109+
67110
/// <summary>
68111
/// Maps a CLR type to a PostgreSQL composite type.
69112
/// </summary>

src/Npgsql/TypeMapping/UserTypeMapper.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,27 @@ sealed class UserTypeMapper
5757
where TEnum : struct, Enum
5858
=> Unmap(typeof(TEnum), out _, pgName, nameTranslator ?? DefaultNameTranslator);
5959

60+
[UnconditionalSuppressMessage("Trimming", "IL2111", Justification = "MapEnum<TEnum> TEnum has less DAM annotations than clrType.")]
61+
[RequiresDynamicCode("Calling MapEnum with a Type can require creating new generic types or methods. This may not work when AOT compiling.")]
62+
public UserTypeMapper MapEnum([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]Type clrType, string? pgName = null, INpgsqlNameTranslator? nameTranslator = null)
63+
{
64+
if (!clrType.IsEnum || !clrType.IsValueType)
65+
throw new ArgumentException("Type must be a concrete Enum", nameof(clrType));
66+
67+
var openMethod = typeof(UserTypeMapper).GetMethod(nameof(MapEnum), new[] { typeof(string), typeof(INpgsqlNameTranslator) })!;
68+
var method = openMethod.MakeGenericMethod(clrType);
69+
method.Invoke(this, new object?[] { pgName, nameTranslator });
70+
return this;
71+
}
72+
73+
public bool UnmapEnum([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)]Type clrType,string? pgName = null, INpgsqlNameTranslator? nameTranslator = null)
74+
{
75+
if (!clrType.IsEnum || !clrType.IsValueType)
76+
throw new ArgumentException("Type must be a concrete Enum", nameof(clrType));
77+
78+
return Unmap(clrType, out _, pgName, nameTranslator ?? DefaultNameTranslator);
79+
}
80+
6081
[RequiresDynamicCode("Mapping composite types involves serializing arbitrary types, requiring require creating new generic types or methods. This is currently unsupported with NativeAOT, vote on issue #5303 if this is important to you.")]
6182
public UserTypeMapper MapComposite<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)] T>(
6283
string? pgName = null, INpgsqlNameTranslator? nameTranslator = null) where T : class

test/Npgsql.Tests/GlobalTypeMapperTests.cs

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,45 @@ public async Task MapEnum()
3434
// Global mapping changes have no effect on already-built data sources
3535
await AssertType(dataSource1, Mood.Happy, "happy", type, npgsqlDbType: null);
3636

37-
// But they do affect on new data sources
37+
// But they do affect new data sources
3838
await using var dataSource2 = CreateDataSource();
3939
await AssertType(dataSource2, "happy", "happy", type, npgsqlDbType: null, isDefault: false);
4040
}
4141

42+
[Test]
43+
public async Task MapEnum_NonGeneric()
44+
{
45+
await using var adminConnection = await OpenConnectionAsync();
46+
var type = await GetTempTypeName(adminConnection);
47+
NpgsqlConnection.GlobalTypeMapper.MapEnum(typeof(Mood), type);
48+
49+
try
50+
{
51+
await using var dataSource1 = CreateDataSource();
52+
53+
await using (var connection = await dataSource1.OpenConnectionAsync())
54+
{
55+
await connection.ExecuteNonQueryAsync($"CREATE TYPE {type} AS ENUM ('sad', 'ok', 'happy')");
56+
await connection.ReloadTypesAsync();
57+
58+
await AssertType(connection, Mood.Happy, "happy", type, npgsqlDbType: null);
59+
}
60+
61+
NpgsqlConnection.GlobalTypeMapper.UnmapEnum(typeof(Mood), type);
62+
63+
// Global mapping changes have no effect on already-built data sources
64+
await AssertType(dataSource1, Mood.Happy, "happy", type, npgsqlDbType: null);
65+
66+
// But they do affect new data sources
67+
await using var dataSource2 = CreateDataSource();
68+
Assert.ThrowsAsync<InvalidCastException>(() => AssertType(dataSource2, Mood.Happy, "happy", type, npgsqlDbType: null));
69+
}
70+
finally
71+
{
72+
NpgsqlConnection.GlobalTypeMapper.UnmapEnum<Mood>(type);
73+
}
74+
}
75+
4276
[Test]
4377
public async Task Reset()
4478
{
@@ -60,7 +94,7 @@ public async Task Reset()
6094
// Global mapping changes have no effect on already-built data sources
6195
await AssertType(dataSource1, Mood.Happy, "happy", type, npgsqlDbType: null);
6296

63-
// But they do affect on new data sources
97+
// But they do affect new data sources
6498
await using var dataSource2 = CreateDataSource();
6599
await AssertType(dataSource2, "happy", "happy", type, npgsqlDbType: null, isDefault: false);
66100
}

test/Npgsql.Tests/Types/EnumTests.cs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,53 @@ public async Task Data_source_mapping()
2929
await AssertType(dataSource, Mood.Happy, "happy", type, npgsqlDbType: null);
3030
}
3131

32+
[Test]
33+
public async Task Data_source_unmap()
34+
{
35+
await using var adminConnection = await OpenConnectionAsync();
36+
var type = await GetTempTypeName(adminConnection);
37+
await adminConnection.ExecuteNonQueryAsync($"CREATE TYPE {type} AS ENUM ('sad', 'ok', 'happy')");
38+
39+
var dataSourceBuilder = CreateDataSourceBuilder();
40+
dataSourceBuilder.MapEnum<Mood>(type);
41+
42+
var isUnmapSuccessful = dataSourceBuilder.UnmapEnum<Mood>(type);
43+
await using var dataSource = dataSourceBuilder.Build();
44+
45+
Assert.IsTrue(isUnmapSuccessful);
46+
Assert.ThrowsAsync<InvalidCastException>(() => AssertType(dataSource, Mood.Happy, "happy", type, npgsqlDbType: null));
47+
}
48+
49+
[Test]
50+
public async Task Data_source_mapping_non_generic()
51+
{
52+
await using var adminConnection = await OpenConnectionAsync();
53+
var type = await GetTempTypeName(adminConnection);
54+
await adminConnection.ExecuteNonQueryAsync($"CREATE TYPE {type} AS ENUM ('sad', 'ok', 'happy')");
55+
56+
var dataSourceBuilder = CreateDataSourceBuilder();
57+
dataSourceBuilder.MapEnum(typeof(Mood), type);
58+
await using var dataSource = dataSourceBuilder.Build();
59+
await AssertType(dataSource, Mood.Happy, "happy", type, npgsqlDbType: null);
60+
}
61+
62+
[Test]
63+
public async Task Data_source_unmap_non_generic()
64+
{
65+
await using var adminConnection = await OpenConnectionAsync();
66+
var type = await GetTempTypeName(adminConnection);
67+
await adminConnection.ExecuteNonQueryAsync($"CREATE TYPE {type} AS ENUM ('sad', 'ok', 'happy')");
68+
69+
var dataSourceBuilder = CreateDataSourceBuilder();
70+
dataSourceBuilder.MapEnum(typeof(Mood), type);
71+
72+
var isUnmapSuccessful = dataSourceBuilder.UnmapEnum(typeof(Mood), type);
73+
await using var dataSource = dataSourceBuilder.Build();
74+
75+
Assert.IsTrue(isUnmapSuccessful);
76+
Assert.ThrowsAsync<InvalidCastException>(() => AssertType(dataSource, Mood.Happy, "happy", type, npgsqlDbType: null));
77+
}
78+
3279
[Test]
3380
public async Task Dual_enums()
3481
{

0 commit comments

Comments
 (0)