Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/Npgsql/NpgsqlDataSourceBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,17 @@ void INpgsqlTypeMapper.Reset()
where TEnum : struct, Enum
=> _internalBuilder.UnmapEnum<TEnum>(pgName, nameTranslator);

/// <inheritdoc />
[RequiresDynamicCode("Calling MapEnum with a Type can require creating new generic types or methods. This may not work when AOT compiling.")]
public INpgsqlTypeMapper MapEnum([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
Type clrType, string? pgName = null, INpgsqlNameTranslator? nameTranslator = null)
=> _internalBuilder.MapEnum(clrType, pgName, nameTranslator);

/// <inheritdoc />
public bool UnmapEnum([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
Type clrType, string? pgName = null, INpgsqlNameTranslator? nameTranslator = null)
=> _internalBuilder.UnmapEnum(clrType, pgName, nameTranslator);

/// <inheritdoc />
[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.")]
public INpgsqlTypeMapper MapComposite<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicFields)] T>(
Expand Down
14 changes: 14 additions & 0 deletions src/Npgsql/NpgsqlSlimDataSourceBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,20 @@ public INpgsqlNameTranslator DefaultNameTranslator
where TEnum : struct, Enum
=> _userTypeMapper.UnmapEnum<TEnum>(pgName, nameTranslator);

/// <inheritdoc />
[RequiresDynamicCode("Calling MapEnum with a Type can require creating new generic types or methods. This may not work when AOT compiling.")]
public INpgsqlTypeMapper MapEnum([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
Type clrType, string? pgName = null, INpgsqlNameTranslator? nameTranslator = null)
{
_userTypeMapper.MapEnum(clrType, pgName, nameTranslator);
return this;
}

/// <inheritdoc />
public bool UnmapEnum([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
Type clrType, string? pgName = null, INpgsqlNameTranslator? nameTranslator = null)
=> _userTypeMapper.UnmapEnum(clrType, pgName, nameTranslator);

/// <inheritdoc />
[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.")]
public INpgsqlTypeMapper MapComposite<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicFields)] T>(
Expand Down
6 changes: 6 additions & 0 deletions src/Npgsql/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ Npgsql.NpgsqlConnectionStringBuilder.ChannelBinding.set -> void
Npgsql.NpgsqlBinaryImporter.WriteRow(params object?[]! values) -> void
Npgsql.NpgsqlBinaryImporter.WriteRowAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken), params object?[]! values) -> System.Threading.Tasks.Task!
Npgsql.NpgsqlDataSourceBuilder.AddTypeInfoResolver(Npgsql.Internal.IPgTypeInfoResolver! resolver) -> void
Npgsql.NpgsqlDataSourceBuilder.MapEnum(System.Type! clrType, string? pgName = null, Npgsql.INpgsqlNameTranslator? nameTranslator = null) -> Npgsql.TypeMapping.INpgsqlTypeMapper!
Npgsql.NpgsqlDataSourceBuilder.Name.get -> string?
Npgsql.NpgsqlDataSourceBuilder.Name.set -> void
Npgsql.NpgsqlDataSourceBuilder.UnmapEnum(System.Type! clrType, string? pgName = null, Npgsql.INpgsqlNameTranslator? nameTranslator = null) -> bool
Npgsql.NpgsqlDataSourceBuilder.UseRootCertificate(System.Security.Cryptography.X509Certificates.X509Certificate2? rootCertificate) -> Npgsql.NpgsqlDataSourceBuilder!
Npgsql.NpgsqlDataSourceBuilder.UseRootCertificateCallback(System.Func<System.Security.Cryptography.X509Certificates.X509Certificate2!>? rootCertificateCallback) -> Npgsql.NpgsqlDataSourceBuilder!
Npgsql.NpgsqlSlimDataSourceBuilder
Expand All @@ -35,12 +37,14 @@ Npgsql.NpgsqlSlimDataSourceBuilder.EnableRecords() -> Npgsql.NpgsqlSlimDataSourc
Npgsql.NpgsqlSlimDataSourceBuilder.EnableTransportSecurity() -> Npgsql.NpgsqlSlimDataSourceBuilder!
Npgsql.NpgsqlSlimDataSourceBuilder.MapComposite(System.Type! clrType, string? pgName = null, Npgsql.INpgsqlNameTranslator? nameTranslator = null) -> Npgsql.TypeMapping.INpgsqlTypeMapper!
Npgsql.NpgsqlSlimDataSourceBuilder.MapComposite<T>(string? pgName = null, Npgsql.INpgsqlNameTranslator? nameTranslator = null) -> Npgsql.TypeMapping.INpgsqlTypeMapper!
Npgsql.NpgsqlSlimDataSourceBuilder.MapEnum(System.Type! clrType, string? pgName = null, Npgsql.INpgsqlNameTranslator? nameTranslator = null) -> Npgsql.TypeMapping.INpgsqlTypeMapper!
Npgsql.NpgsqlSlimDataSourceBuilder.MapEnum<TEnum>(string? pgName = null, Npgsql.INpgsqlNameTranslator? nameTranslator = null) -> Npgsql.TypeMapping.INpgsqlTypeMapper!
Npgsql.NpgsqlSlimDataSourceBuilder.Name.get -> string?
Npgsql.NpgsqlSlimDataSourceBuilder.Name.set -> void
Npgsql.NpgsqlSlimDataSourceBuilder.NpgsqlSlimDataSourceBuilder(string? connectionString = null) -> void
Npgsql.NpgsqlSlimDataSourceBuilder.UnmapComposite(System.Type! clrType, string? pgName = null, Npgsql.INpgsqlNameTranslator? nameTranslator = null) -> bool
Npgsql.NpgsqlSlimDataSourceBuilder.UnmapComposite<T>(string? pgName = null, Npgsql.INpgsqlNameTranslator? nameTranslator = null) -> bool
Npgsql.NpgsqlSlimDataSourceBuilder.UnmapEnum(System.Type! clrType, string? pgName = null, Npgsql.INpgsqlNameTranslator? nameTranslator = null) -> bool
Npgsql.NpgsqlSlimDataSourceBuilder.UnmapEnum<TEnum>(string? pgName = null, Npgsql.INpgsqlNameTranslator? nameTranslator = null) -> bool
Npgsql.NpgsqlSlimDataSourceBuilder.UseClientCertificate(System.Security.Cryptography.X509Certificates.X509Certificate? clientCertificate) -> Npgsql.NpgsqlSlimDataSourceBuilder!
Npgsql.NpgsqlSlimDataSourceBuilder.UseClientCertificates(System.Security.Cryptography.X509Certificates.X509CertificateCollection? clientCertificates) -> Npgsql.NpgsqlSlimDataSourceBuilder!
Expand All @@ -58,6 +62,8 @@ Npgsql.Replication.PhysicalReplicationConnection.StartReplication(NpgsqlTypes.Np
Npgsql.Replication.PhysicalReplicationSlot.PhysicalReplicationSlot(string! slotName, NpgsqlTypes.NpgsqlLogSequenceNumber? restartLsn = null, uint? restartTimeline = null) -> void
Npgsql.Replication.PhysicalReplicationSlot.RestartTimeline.get -> uint?
Npgsql.TypeMapping.INpgsqlTypeMapper.AddTypeInfoResolver(Npgsql.Internal.IPgTypeInfoResolver! resolver) -> void
Npgsql.TypeMapping.INpgsqlTypeMapper.MapEnum(System.Type! clrType, string? pgName = null, Npgsql.INpgsqlNameTranslator? nameTranslator = null) -> Npgsql.TypeMapping.INpgsqlTypeMapper!
Npgsql.TypeMapping.INpgsqlTypeMapper.UnmapEnum(System.Type! clrType, string? pgName = null, Npgsql.INpgsqlNameTranslator? nameTranslator = null) -> bool
Npgsql.TypeMapping.UserTypeMapping
Npgsql.TypeMapping.UserTypeMapping.ClrType.get -> System.Type!
Npgsql.TypeMapping.UserTypeMapping.PgTypeName.get -> string!
Expand Down
35 changes: 35 additions & 0 deletions src/Npgsql/TypeMapping/GlobalTypeMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,41 @@ public INpgsqlNameTranslator DefaultNameTranslator
}
}

/// <inheritdoc />
[RequiresDynamicCode("Calling MapEnum with a Type can require creating new generic types or methods. This may not work when AOT compiling.")]
public INpgsqlTypeMapper MapEnum([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
Type clrType, string? pgName = null, INpgsqlNameTranslator? nameTranslator = null)
{
_lock.EnterWriteLock();
try
{
_userTypeMapper.MapEnum(clrType, pgName, nameTranslator);
ResetTypeMappingCache();
return this;
}
finally
{
_lock.ExitWriteLock();
}
}

/// <inheritdoc />
public bool UnmapEnum([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
Type clrType, string? pgName = null, INpgsqlNameTranslator? nameTranslator = null)
{
_lock.EnterWriteLock();
try
{
var result = _userTypeMapper.UnmapEnum(clrType, pgName, nameTranslator);
ResetTypeMappingCache();
return result;
}
finally
{
_lock.ExitWriteLock();
}
}

/// <inheritdoc />
[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.")]
public INpgsqlTypeMapper MapComposite<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicFields)] T>(string? pgName = null, INpgsqlNameTranslator? nameTranslator = null)
Expand Down
43 changes: 43 additions & 0 deletions src/Npgsql/TypeMapping/INpgsqlTypeMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,49 @@ public interface INpgsqlTypeMapper
INpgsqlNameTranslator? nameTranslator = null)
where TEnum : struct, Enum;

/// <summary>
/// Maps a CLR enum to a PostgreSQL enum type.
/// </summary>
/// <remarks>
/// CLR enum labels are mapped by name to PostgreSQL enum labels.
/// The translation strategy can be controlled by the <paramref name="nameTranslator"/> parameter,
/// which defaults to <see cref="NpgsqlSnakeCaseNameTranslator"/>.
/// You can also use the <see cref="PgNameAttribute"/> on your enum fields to manually specify a PostgreSQL enum label.
/// If there is a discrepancy between the .NET and database labels while an enum is read or written,
/// an exception will be raised.
/// </remarks>
/// <param name="clrType">The .NET enum type to be mapped</param>
/// <param name="pgName">
/// A PostgreSQL type name for the corresponding enum type in the database.
/// If null, the name translator given in <paramref name="nameTranslator"/> will be used.
/// </param>
/// <param name="nameTranslator">
/// A component which will be used to translate CLR names (e.g. SomeClass) into database names (e.g. some_class).
/// Defaults to <see cref="DefaultNameTranslator" />.
/// </param>
[RequiresDynamicCode("Calling MapEnum with a Type can require creating new generic types or methods. This may not work when AOT compiling.")]
INpgsqlTypeMapper MapEnum(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]Type clrType,
string? pgName = null,
INpgsqlNameTranslator? nameTranslator = null);

/// <summary>
/// Removes an existing enum mapping.
/// </summary>
/// <param name="clrType">The .NET enum type to be mapped</param>
/// <param name="pgName">
/// A PostgreSQL type name for the corresponding enum type in the database.
/// If null, the name translator given in <paramref name="nameTranslator"/> will be used.
/// </param>
/// <param name="nameTranslator">
/// A component which will be used to translate CLR names (e.g. SomeClass) into database names (e.g. some_class).
/// Defaults to <see cref="DefaultNameTranslator" />.
/// </param>
bool UnmapEnum(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]Type clrType,
string? pgName = null,
INpgsqlNameTranslator? nameTranslator = null);

/// <summary>
/// Maps a CLR type to a PostgreSQL composite type.
/// </summary>
Expand Down
21 changes: 21 additions & 0 deletions src/Npgsql/TypeMapping/UserTypeMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,27 @@ sealed class UserTypeMapper
where TEnum : struct, Enum
=> Unmap(typeof(TEnum), out _, pgName, nameTranslator ?? DefaultNameTranslator);

[UnconditionalSuppressMessage("Trimming", "IL2111", Justification = "MapEnum<TEnum> TEnum has less DAM annotations than clrType.")]
[RequiresDynamicCode("Calling MapEnum with a Type can require creating new generic types or methods. This may not work when AOT compiling.")]
public UserTypeMapper MapEnum([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]Type clrType, string? pgName = null, INpgsqlNameTranslator? nameTranslator = null)
{
if (!clrType.IsEnum || !clrType.IsValueType)
throw new ArgumentException("Type must be a concrete Enum", nameof(clrType));

var openMethod = typeof(UserTypeMapper).GetMethod(nameof(MapEnum), new[] { typeof(string), typeof(INpgsqlNameTranslator) })!;
var method = openMethod.MakeGenericMethod(clrType);
method.Invoke(this, new object?[] { pgName, nameTranslator });
return this;
}

public bool UnmapEnum([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)]Type clrType,string? pgName = null, INpgsqlNameTranslator? nameTranslator = null)
{
if (!clrType.IsEnum || !clrType.IsValueType)
throw new ArgumentException("Type must be a concrete Enum", nameof(clrType));

return Unmap(clrType, out _, pgName, nameTranslator ?? DefaultNameTranslator);
}

[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.")]
public UserTypeMapper MapComposite<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)] T>(
string? pgName = null, INpgsqlNameTranslator? nameTranslator = null) where T : class
Expand Down
38 changes: 36 additions & 2 deletions test/Npgsql.Tests/GlobalTypeMapperTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,45 @@ public async Task MapEnum()
// Global mapping changes have no effect on already-built data sources
await AssertType(dataSource1, Mood.Happy, "happy", type, npgsqlDbType: null);

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

[Test]
public async Task MapEnum_NonGeneric()
{
await using var adminConnection = await OpenConnectionAsync();
var type = await GetTempTypeName(adminConnection);
NpgsqlConnection.GlobalTypeMapper.MapEnum(typeof(Mood), type);

try
{
await using var dataSource1 = CreateDataSource();

await using (var connection = await dataSource1.OpenConnectionAsync())
{
await connection.ExecuteNonQueryAsync($"CREATE TYPE {type} AS ENUM ('sad', 'ok', 'happy')");
await connection.ReloadTypesAsync();

await AssertType(connection, Mood.Happy, "happy", type, npgsqlDbType: null);
}

NpgsqlConnection.GlobalTypeMapper.UnmapEnum(typeof(Mood), type);

// Global mapping changes have no effect on already-built data sources
await AssertType(dataSource1, Mood.Happy, "happy", type, npgsqlDbType: null);

// But they do affect new data sources
await using var dataSource2 = CreateDataSource();
Assert.ThrowsAsync<InvalidCastException>(() => AssertType(dataSource2, Mood.Happy, "happy", type, npgsqlDbType: null));
}
finally
{
NpgsqlConnection.GlobalTypeMapper.UnmapEnum<Mood>(type);
}
}

[Test]
public async Task Reset()
{
Expand All @@ -60,7 +94,7 @@ public async Task Reset()
// Global mapping changes have no effect on already-built data sources
await AssertType(dataSource1, Mood.Happy, "happy", type, npgsqlDbType: null);

// But they do affect on new data sources
// But they do affect new data sources
await using var dataSource2 = CreateDataSource();
await AssertType(dataSource2, "happy", "happy", type, npgsqlDbType: null, isDefault: false);
}
Expand Down
47 changes: 47 additions & 0 deletions test/Npgsql.Tests/Types/EnumTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,53 @@ public async Task Data_source_mapping()
await AssertType(dataSource, Mood.Happy, "happy", type, npgsqlDbType: null);
}

[Test]
public async Task Data_source_unmap()
{
await using var adminConnection = await OpenConnectionAsync();
var type = await GetTempTypeName(adminConnection);
await adminConnection.ExecuteNonQueryAsync($"CREATE TYPE {type} AS ENUM ('sad', 'ok', 'happy')");

var dataSourceBuilder = CreateDataSourceBuilder();
dataSourceBuilder.MapEnum<Mood>(type);

var isUnmapSuccessful = dataSourceBuilder.UnmapEnum<Mood>(type);
await using var dataSource = dataSourceBuilder.Build();

Assert.IsTrue(isUnmapSuccessful);
Assert.ThrowsAsync<InvalidCastException>(() => AssertType(dataSource, Mood.Happy, "happy", type, npgsqlDbType: null));
}

[Test]
public async Task Data_source_mapping_non_generic()
{
await using var adminConnection = await OpenConnectionAsync();
var type = await GetTempTypeName(adminConnection);
await adminConnection.ExecuteNonQueryAsync($"CREATE TYPE {type} AS ENUM ('sad', 'ok', 'happy')");

var dataSourceBuilder = CreateDataSourceBuilder();
dataSourceBuilder.MapEnum(typeof(Mood), type);
await using var dataSource = dataSourceBuilder.Build();
await AssertType(dataSource, Mood.Happy, "happy", type, npgsqlDbType: null);
}

[Test]
public async Task Data_source_unmap_non_generic()
{
await using var adminConnection = await OpenConnectionAsync();
var type = await GetTempTypeName(adminConnection);
await adminConnection.ExecuteNonQueryAsync($"CREATE TYPE {type} AS ENUM ('sad', 'ok', 'happy')");

var dataSourceBuilder = CreateDataSourceBuilder();
dataSourceBuilder.MapEnum(typeof(Mood), type);

var isUnmapSuccessful = dataSourceBuilder.UnmapEnum(typeof(Mood), type);
await using var dataSource = dataSourceBuilder.Build();

Assert.IsTrue(isUnmapSuccessful);
Assert.ThrowsAsync<InvalidCastException>(() => AssertType(dataSource, Mood.Happy, "happy", type, npgsqlDbType: null));
}

[Test]
public async Task Dual_enums()
{
Expand Down