未验证 提交 2e06eb40 编写于 作者: S Shay Rojansky 提交者: GitHub

Fixup to xmin/concurrency token changes (#2546)

* Support value-converted concurrency tokens
* Obsolete UseXminAsConcurrencyToken
* Improve testing and align with upstream

Continues #2543
Fixes #2494
上级 7886196b
......@@ -11,45 +11,6 @@ namespace Microsoft.EntityFrameworkCore;
/// </remarks>
public static class NpgsqlEntityTypeBuilderExtensions
{
#region xmin
/// <summary>
/// Configures using the auto-updating system column <c>xmin</c> as the optimistic concurrency token.
/// </summary>
/// <param name="entityTypeBuilder">The builder for the entity type being configured.</param>
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
/// <remarks>
/// See <see href="https://www.npgsql.org/efcore/modeling/concurrency.html">Concurrency tokens</see>
/// for more information on using optimistic concurrency in PostgreSQL.
/// </remarks>
public static EntityTypeBuilder UseXminAsConcurrencyToken(
this EntityTypeBuilder entityTypeBuilder)
{
Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder));
entityTypeBuilder.Property<uint>("xmin")
.HasColumnType("xid")
.ValueGeneratedOnAddOrUpdate()
.IsConcurrencyToken();
return entityTypeBuilder;
}
/// <summary>
/// Configures using the auto-updating system column <c>xmin</c> as the optimistic concurrency token.
/// </summary>
/// <remarks>
/// See http://www.npgsql.org/efcore/miscellaneous.html#optimistic-concurrency-and-concurrency-tokens
/// </remarks>
/// <param name="entityTypeBuilder">The builder for the entity type being configured.</param>
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
public static EntityTypeBuilder<TEntity> UseXminAsConcurrencyToken<TEntity>(
this EntityTypeBuilder<TEntity> entityTypeBuilder)
where TEntity : class
=> (EntityTypeBuilder<TEntity>)UseXminAsConcurrencyToken((EntityTypeBuilder)entityTypeBuilder);
#endregion xmin
#region Generated tsvector column
// Note: actual configuration for generated TsVector properties is on the property
......@@ -334,77 +295,41 @@ public static class NpgsqlEntityTypeBuilderExtensions
#region Obsolete
/// <summary>
/// Sets a PostgreSQL storage parameter on the table created for this entity.
/// Configures using the auto-updating system column <c>xmin</c> as the optimistic concurrency token.
/// </summary>
/// <param name="entityTypeBuilder">The builder for the entity type being configured.</param>
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
/// <remarks>
/// See https://www.postgresql.org/docs/current/static/sql-createtable.html#SQL-CREATETABLE-STORAGE-PARAMETERS
/// See <see href="https://www.npgsql.org/efcore/modeling/concurrency.html">Concurrency tokens</see>
/// for more information on using optimistic concurrency in PostgreSQL.
/// </remarks>
/// <param name="entityTypeBuilder"> The builder for the entity type being configured. </param>
/// <param name="parameterName"> The name of the storage parameter. </param>
/// <param name="parameterValue"> The value of the storage parameter. </param>
/// <returns> The same builder instance so that multiple calls can be chained. </returns>
[Obsolete("Use HasStorageParameter")]
public static EntityTypeBuilder SetStorageParameter(
this EntityTypeBuilder entityTypeBuilder,
string parameterName,
object? parameterValue)
=> HasStorageParameter(entityTypeBuilder, parameterName, parameterValue);
[Obsolete("Use EF Core's standard IsRowVersion() or [Timestamp], see https://learn.microsoft.com/ef/core/saving/concurrency")]
public static EntityTypeBuilder UseXminAsConcurrencyToken(
this EntityTypeBuilder entityTypeBuilder)
{
Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder));
/// <summary>
/// Sets a PostgreSQL storage parameter on the table created for this entity.
/// </summary>
/// <remarks>
/// See https://www.postgresql.org/docs/current/static/sql-createtable.html#SQL-CREATETABLE-STORAGE-PARAMETERS
/// </remarks>
/// <param name="entityTypeBuilder"> The builder for the entity type being configured. </param>
/// <param name="parameterName"> The name of the storage parameter. </param>
/// <param name="parameterValue"> The value of the storage parameter. </param>
/// <returns> The same builder instance so that multiple calls can be chained. </returns>
[Obsolete("Use HasStorageParameter")]
public static EntityTypeBuilder<TEntity> SetStorageParameter<TEntity>(
this EntityTypeBuilder<TEntity> entityTypeBuilder,
string parameterName,
object? parameterValue)
where TEntity : class
=> HasStorageParameter(entityTypeBuilder, parameterName, parameterValue);
entityTypeBuilder.Property<uint>("xmin")
.HasColumnType("xid")
.ValueGeneratedOnAddOrUpdate()
.IsConcurrencyToken();
/// <summary>
/// Sets a PostgreSQL storage parameter on the table created for this entity.
/// </summary>
/// <remarks>
/// See https://www.postgresql.org/docs/current/static/sql-createtable.html#SQL-CREATETABLE-STORAGE-PARAMETERS
/// </remarks>
/// <param name="entityTypeBuilder"> The builder for the entity type being configured. </param>
/// <param name="parameterName"> The name of the storage parameter. </param>
/// <param name="parameterValue"> The value of the storage parameter. </param>
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
/// <returns> The same builder instance so that multiple calls can be chained. </returns>
[Obsolete("Use HasStorageParameter")]
public static IConventionEntityTypeBuilder? SetStorageParameter(
this IConventionEntityTypeBuilder entityTypeBuilder,
string parameterName,
object? parameterValue,
bool fromDataAnnotation = false)
=> HasStorageParameter(entityTypeBuilder, parameterName, parameterValue, fromDataAnnotation);
return entityTypeBuilder;
}
/// <summary>
/// Returns a value indicating whether the PostgreSQL storage parameter on the table created for this entity.
/// Configures using the auto-updating system column <c>xmin</c> as the optimistic concurrency token.
/// </summary>
/// <remarks>
/// See https://www.postgresql.org/docs/current/static/sql-createtable.html#SQL-CREATETABLE-STORAGE-PARAMETERS
/// See http://www.npgsql.org/efcore/miscellaneous.html#optimistic-concurrency-and-concurrency-tokens
/// </remarks>
/// <param name="entityTypeBuilder"> The builder for the entity type being configured. </param>
/// <param name="parameterName"> The name of the storage parameter. </param>
/// <param name="parameterValue"> The value of the storage parameter. </param>
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
/// <returns><c>true</c> if the mapped table can be configured as with the storage parameter.</returns>
[Obsolete("Use CanSetStorageParameter")]
public static bool CanSetSetStorageParameter(
this IConventionEntityTypeBuilder entityTypeBuilder,
string parameterName,
object? parameterValue,
bool fromDataAnnotation = false)
=> CanSetStorageParameter(entityTypeBuilder, parameterName, parameterValue, fromDataAnnotation);
/// <param name="entityTypeBuilder">The builder for the entity type being configured.</param>
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
[Obsolete("Use EF Core's standard IsRowVersion() or [Timestamp], see https://learn.microsoft.com/ef/core/saving/concurrency")]
public static EntityTypeBuilder<TEntity> UseXminAsConcurrencyToken<TEntity>(
this EntityTypeBuilder<TEntity> entityTypeBuilder)
where TEntity : class
=> (EntityTypeBuilder<TEntity>)UseXminAsConcurrencyToken((EntityTypeBuilder)entityTypeBuilder);
#endregion Obsolete
}
......@@ -29,8 +29,14 @@ public virtual void ProcessModelFinalizing(IConventionModelBuilder modelBuilder,
{
foreach (var property in entityType.GetDeclaredProperties())
{
DiscoverPostgresExtensions(property, modelBuilder);
ProcessRowVersionProperty(property);
var typeMapping = (RelationalTypeMapping?)property.FindTypeMapping()
?? _typeMappingSource.FindMapping((IProperty)property);
if (typeMapping is not null)
{
DiscoverPostgresExtensions(property, typeMapping, modelBuilder);
ProcessRowVersionProperty(property, typeMapping);
}
}
}
}
......@@ -38,29 +44,24 @@ public virtual void ProcessModelFinalizing(IConventionModelBuilder modelBuilder,
/// <summary>
/// Discovers certain common PostgreSQL extensions based on property store types (e.g. hstore).
/// </summary>
/// <param name="property"></param>
/// <param name="modelBuilder"></param>
protected virtual void DiscoverPostgresExtensions(IConventionProperty property, IConventionModelBuilder modelBuilder)
protected virtual void DiscoverPostgresExtensions(
IConventionProperty property,
RelationalTypeMapping typeMapping,
IConventionModelBuilder modelBuilder)
{
var typeMapping = (RelationalTypeMapping?)property.FindTypeMapping()
?? _typeMappingSource.FindMapping((IProperty)property);
if (typeMapping is not null)
switch (typeMapping.StoreType)
{
switch (typeMapping.StoreType)
{
case "hstore":
modelBuilder.HasPostgresExtension("hstore");
break;
case "citext":
modelBuilder.HasPostgresExtension("citext");
break;
case "ltree":
case "lquery":
case "ltxtquery":
modelBuilder.HasPostgresExtension("ltree");
break;
}
case "hstore":
modelBuilder.HasPostgresExtension("hstore");
break;
case "citext":
modelBuilder.HasPostgresExtension("citext");
break;
case "ltree":
case "lquery":
case "ltxtquery":
modelBuilder.HasPostgresExtension("ltree");
break;
}
}
......@@ -68,14 +69,13 @@ protected virtual void DiscoverPostgresExtensions(IConventionProperty property,
/// Detects properties which are uint, OnAddOrUpdate and configured as concurrency tokens, and maps these to the PostgreSQL
/// internal "xmin" column, which changes every time the row is modified.
/// </summary>
protected virtual void ProcessRowVersionProperty(IConventionProperty property)
protected virtual void ProcessRowVersionProperty(IConventionProperty property, RelationalTypeMapping typeMapping)
{
if (property.ValueGenerated == ValueGenerated.OnAddOrUpdate
&& property.IsConcurrencyToken
&& property.ClrType == typeof(uint)
&& property.GetValueConverter() is null)
&& typeMapping.StoreType == "xid")
{
property.Builder.HasColumnName("xmin")?.HasColumnType("xid");
property.Builder.HasColumnName("xmin");
}
}
}
......@@ -574,34 +574,49 @@ protected virtual void SetupEnumMappings(ISqlGenerationHelper sqlGenerationHelpe
// we proceed with a CLR type lookup (if the type doesn't exist at all the failure will come later).
}
if (clrType is null ||
!ClrTypeMappings.TryGetValue(clrType, out var mapping) ||
// Special case for byte[] mapped as smallint[] - don't return bytea mapping
storeTypeName is not null && storeTypeName == "smallint[]")
if (clrType is not null)
{
return null;
}
if (mappingInfo.Size.HasValue)
{
if (clrType == typeof(string))
if (ClrTypeMappings.TryGetValue(clrType, out var mapping))
{
mapping = mappingInfo.IsFixedLength ?? false ? _char : _varchar;
// Handle types with the size facet (string, bitarray)
if (mappingInfo.Size.HasValue)
{
if (clrType == typeof(string))
{
mapping = mappingInfo.IsFixedLength ?? false ? _char : _varchar;
// See #342 for when size > 10485760
return mappingInfo.Size <= 10485760
? mapping.Clone($"{mapping.StoreType}({mappingInfo.Size})", mappingInfo.Size)
: _text;
}
// See #342 for when size > 10485760
return mappingInfo.Size <= 10485760
? mapping.Clone($"{mapping.StoreType}({mappingInfo.Size})", mappingInfo.Size)
: _text;
if (clrType == typeof(BitArray))
{
mapping = mappingInfo.IsFixedLength ?? false ? _bit : _varbit;
return mapping.Clone($"{mapping.StoreType}({mappingInfo.Size})", mappingInfo.Size);
}
}
if (storeTypeName == "smallint[]" && clrType == typeof(byte[]))
{
// PostgreSQL has no tinyint (single-byte) type, but we allow mapping CLR byte to PG smallint (2-bytes).
// The same applies to arrays - as always - so byte[] should be mappable to smallint[].
// However, byte[] also has a base mapping to bytea, which is the default. So when the user explicitly specified
// mapping to smallint[], we don't return that to allow the array mapping to work.
return null;
}
return mapping;
}
if (clrType == typeof(BitArray))
if (clrType == typeof(uint) && mappingInfo.IsRowVersion == true)
{
mapping = mappingInfo.IsFixedLength ?? false ? _bit : _varbit;
return mapping.Clone($"{mapping.StoreType}({mappingInfo.Size})", mappingInfo.Size);
return _xid;
}
}
return mapping;
return null;
}
/// <summary>
......
namespace Npgsql.EntityFrameworkCore.PostgreSQL;
public class DatabindingNpgsqlTest : DatabindingTestBase<F1NpgsqlFixture>
public class DatabindingNpgsqlTest : DatabindingTestBase<F1BytesNpgsqlFixture>
{
public DatabindingNpgsqlTest(F1NpgsqlFixture fixture)
public DatabindingNpgsqlTest(F1BytesNpgsqlFixture fixture)
: base(fixture)
{
}
}
\ No newline at end of file
}
......@@ -3,27 +3,58 @@
namespace Npgsql.EntityFrameworkCore.PostgreSQL;
public class F1UIntNpgsqlFixture : F1NpgsqlFixtureBase<uint?>
public class F1BytesNpgsqlFixture : F1NpgsqlFixtureBase<byte[]>
{
}
protected override void BuildModelExternal(ModelBuilder modelBuilder)
{
base.BuildModelExternal(modelBuilder);
public class F1NpgsqlFixture : F1NpgsqlFixtureBase<byte[]>
{
modelBuilder.Entity<Chassis>().Property<byte[]>("Version").HasConversion<BytesToUIntConverter>(new ArrayStructuralComparer<byte>());
modelBuilder.Entity<Driver>().Property<byte[]>("Version").HasConversion<BytesToUIntConverter>(new ArrayStructuralComparer<byte>());
modelBuilder.Entity<Team>().Property<byte[]>("Version").HasConversion<BytesToUIntConverter>(new ArrayStructuralComparer<byte>());
modelBuilder.Entity<Sponsor>().Property<byte[]>("Version").HasConversion<BytesToUIntConverter>(new ArrayStructuralComparer<byte>());
modelBuilder.Entity<TitleSponsor>()
.OwnsOne(
s => s.Details, eb =>
{
eb.Property<byte[]>("Version").IsRowVersion().HasConversion<BytesToUIntConverter>(new ArrayStructuralComparer<byte>());
});
}
private class BytesToUIntConverter : ValueConverter<byte[], uint>
{
public BytesToUIntConverter()
: base(
bytes => BitConverter.ToUInt32(bytes),
num => BitConverter.GetBytes(num),
mappingHints: null)
{
}
}
}
// Note that the type parameter determines the type of the Version shadow property in the model,
// but in Npgsql we just use UseXminAsConcurrencyToken.
public abstract class F1NpgsqlFixtureBase<TRowVersion> : F1RelationalFixture<TRowVersion>
public class F1NpgsqlFixture : F1NpgsqlFixtureBase<uint>
{
protected override ITestStoreFactory TestStoreFactory => NpgsqlTestStoreFactory.Instance;
public override TestHelpers TestHelpers => NpgsqlTestHelpers.Instance;
protected override void BuildModelExternal(ModelBuilder modelBuilder)
{
base.BuildModelExternal(modelBuilder);
modelBuilder.Entity<Chassis>().UseXminAsConcurrencyToken();
modelBuilder.Entity<Driver>().UseXminAsConcurrencyToken();
modelBuilder.Entity<Team>().UseXminAsConcurrencyToken();
// TODO: This is a hack to work around, remove in 8.0 after https://github.com/dotnet/efcore/pull/29401
modelBuilder.Entity<Chassis>().Property<uint>("Version").HasConversion((ValueConverter)null);
modelBuilder.Entity<Driver>().Property<uint>("Version").HasConversion((ValueConverter)null);
modelBuilder.Entity<Team>().Property<uint>("Version").HasConversion((ValueConverter)null);
modelBuilder.Entity<Sponsor>().Property<uint>("Version").HasConversion((ValueConverter)null);
modelBuilder.Entity<TitleSponsor>()
.OwnsOne(
s => s.Details, eb =>
{
eb.Property<uint>("Version").IsRowVersion().HasConversion((ValueConverter)null);
});
}
}
\ No newline at end of file
}
public abstract class F1NpgsqlFixtureBase<TRowVersion> : F1RelationalFixture<TRowVersion>
{
protected override ITestStoreFactory TestStoreFactory => NpgsqlTestStoreFactory.Instance;
public override TestHelpers TestHelpers => NpgsqlTestHelpers.Instance;
}
namespace Npgsql.EntityFrameworkCore.PostgreSQL;
using Microsoft.EntityFrameworkCore.TestModels.ConcurrencyModel;
public class OptimisticConcurrencyNpgsqlTest : OptimisticConcurrencyRelationalTestBase<F1UIntNpgsqlFixture, uint?>
namespace Npgsql.EntityFrameworkCore.PostgreSQL;
public class OptimisticConcurrencyBytesNpgsqlTest : OptimisticConcurrencyNpgsqlTestBase<F1BytesNpgsqlFixture, byte[]>
{
public OptimisticConcurrencyNpgsqlTest(F1UIntNpgsqlFixture fixture) : base(fixture) {}
public OptimisticConcurrencyBytesNpgsqlTest(F1BytesNpgsqlFixture fixture)
: base(fixture)
{
}
}
[Fact]
// uint maps directly to xid, which is the PG type of the xmin column that we use as a row version.
public class OptimisticConcurrencyNpgsqlTest : OptimisticConcurrencyNpgsqlTestBase<F1NpgsqlFixture, uint>
{
public OptimisticConcurrencyNpgsqlTest(F1NpgsqlFixture fixture)
: base(fixture)
{
}
}
public abstract class OptimisticConcurrencyNpgsqlTestBase<TFixture, TRowVersion>
: OptimisticConcurrencyRelationalTestBase<TFixture, TRowVersion>
where TFixture : F1RelationalFixture<TRowVersion>, new()
{
protected OptimisticConcurrencyNpgsqlTestBase(TFixture fixture)
: base(fixture)
{
}
[ConditionalFact]
public async Task Modifying_concurrency_token_only_is_noop()
{
using var c = CreateF1Context();
await c.Database.CreateExecutionStrategy().ExecuteAsync(
c, async context =>
{
using var transaction = context.Database.BeginTransaction();
var driver = context.Drivers.Single(d => d.CarNumber == 1);
driver.Podiums = StorePodiums;
var firstVersion = context.Entry(driver).Property<TRowVersion>("Version").CurrentValue;
await context.SaveChangesAsync();
using var innerContext = CreateF1Context();
innerContext.Database.UseTransaction(transaction.GetDbTransaction());
driver = innerContext.Drivers.Single(d => d.CarNumber == 1);
Assert.NotEqual(firstVersion, innerContext.Entry(driver).Property<TRowVersion>("Version").CurrentValue);
Assert.Equal(StorePodiums, driver.Podiums);
var secondVersion = innerContext.Entry(driver).Property<TRowVersion>("Version").CurrentValue;
innerContext.Entry(driver).Property<TRowVersion>("Version").CurrentValue = firstVersion;
await innerContext.SaveChangesAsync();
using var validationContext = CreateF1Context();
validationContext.Database.UseTransaction(transaction.GetDbTransaction());
driver = validationContext.Drivers.Single(d => d.CarNumber == 1);
Assert.Equal(secondVersion, validationContext.Entry(driver).Property<TRowVersion>("Version").CurrentValue);
Assert.Equal(StorePodiums, driver.Podiums);
});
}
[ConditionalFact]
public async Task Database_concurrency_token_value_is_updated_for_all_sharing_entities()
{
using var c = CreateF1Context();
await c.Database.CreateExecutionStrategy().ExecuteAsync(
c, async context =>
{
using var transaction = context.Database.BeginTransaction();
var sponsor = context.Set<TitleSponsor>().Single();
var sponsorEntry = c.Entry(sponsor);
var detailsEntry = sponsorEntry.Reference(s => s.Details).TargetEntry;
var sponsorVersion = sponsorEntry.Property<TRowVersion>("Version").CurrentValue;
var detailsVersion = detailsEntry.Property<TRowVersion>("Version").CurrentValue;
await c.Database.CreateExecutionStrategy().ExecuteAsync(c, async context =>
{
using var transaction = context.Database.BeginTransaction();
var driver = context.Drivers.Single(d => d.CarNumber == 1);
Assert.Null(sponsorEntry.Property<int?>(Sponsor.ClientTokenPropertyName).CurrentValue);
sponsorEntry.Property<int?>(Sponsor.ClientTokenPropertyName).CurrentValue = 1;
Assert.NotEqual(1u, context.Entry(driver).Property<uint>("xmin").CurrentValue);
sponsor.Name = "Telecom";
driver.Podiums = StorePodiums;
var firstVersion = context.Entry(driver).Property<uint>("xmin").CurrentValue;
await context.SaveChangesAsync();
Assert.Equal(sponsorVersion, detailsVersion);
using var innerContext = CreateF1Context();
innerContext.Database.UseTransaction(transaction.GetDbTransaction());
driver = innerContext.Drivers.Single(d => d.CarNumber == 1);
await context.SaveChangesAsync();
Assert.NotEqual(firstVersion, innerContext.Entry(driver).Property<uint>("xmin").CurrentValue);
Assert.Equal(StorePodiums, driver.Podiums);
var newSponsorVersion = sponsorEntry.Property<TRowVersion>("Version").CurrentValue;
var newDetailsVersion = detailsEntry.Property<TRowVersion>("Version").CurrentValue;
var secondVersion = innerContext.Entry(driver).Property<uint>("xmin").CurrentValue;
innerContext.Entry(driver).Property<uint>("xmin").CurrentValue = firstVersion;
await innerContext.SaveChangesAsync();
Assert.Equal(newSponsorVersion, newDetailsVersion);
Assert.NotEqual(sponsorVersion, newSponsorVersion);
using var validationContext = CreateF1Context();
validationContext.Database.UseTransaction(transaction.GetDbTransaction());
driver = validationContext.Drivers.Single(d => d.CarNumber == 1);
Assert.Equal(1, sponsorEntry.Property<int?>(Sponsor.ClientTokenPropertyName).CurrentValue);
Assert.Equal(1, detailsEntry.Property<int?>(Sponsor.ClientTokenPropertyName).CurrentValue);
});
}
[ConditionalFact]
public async Task Original_concurrency_token_value_is_used_when_replacing_owned_instance()
{
using var c = CreateF1Context();
await c.Database.CreateExecutionStrategy().ExecuteAsync(
c, async context =>
{
using var transaction = context.Database.BeginTransaction();
var sponsor = context.Set<TitleSponsor>().Single();
var sponsorEntry = c.Entry(sponsor);
var sponsorVersion = sponsorEntry.Property<TRowVersion>("Version").CurrentValue;
Assert.Null(sponsorEntry.Property<int?>(Sponsor.ClientTokenPropertyName).CurrentValue);
sponsorEntry.Property<int?>(Sponsor.ClientTokenPropertyName).CurrentValue = 1;
sponsor.Details = new SponsorDetails { Days = 11, Space = 51m };
context.ChangeTracker.DetectChanges();
var detailsEntry = sponsorEntry.Reference(s => s.Details).TargetEntry;
detailsEntry.Property<int?>(Sponsor.ClientTokenPropertyName).CurrentValue = 1;
await context.SaveChangesAsync();
Assert.Equal(secondVersion, validationContext.Entry(driver).Property<uint>("xmin").CurrentValue);
Assert.Equal(StorePodiums, driver.Podiums);
});
var newSponsorVersion = sponsorEntry.Property<TRowVersion>("Version").CurrentValue;
var newDetailsVersion = detailsEntry.Property<TRowVersion>("Version").CurrentValue;
Assert.Equal(newSponsorVersion, newDetailsVersion);
Assert.NotEqual(sponsorVersion, newSponsorVersion);
Assert.Equal(1, sponsorEntry.Property<int?>(Sponsor.ClientTokenPropertyName).CurrentValue);
Assert.Equal(1, detailsEntry.Property<int?>(Sponsor.ClientTokenPropertyName).CurrentValue);
});
}
public override void Property_entry_original_value_is_set()
{
base.Property_entry_original_value_is_set();
AssertSql(
"""
SELECT e."Id", e."EngineSupplierId", e."Name", e."StorageLocation_Latitude", e."StorageLocation_Longitude"
FROM "Engines" AS e
ORDER BY e."Id" NULLS FIRST
LIMIT 1
""",
//
"""
@p1='1'
@p2='Mercedes'
@p0='FO 108X'
@p3='ChangedEngine'
@p4='47.64491' (Nullable = true)
@p5='-122.128101' (Nullable = true)
UPDATE "Engines" SET "Name" = @p0
WHERE "Id" = @p1 AND "EngineSupplierId" = @p2 AND "Name" = @p3 AND "StorageLocation_Latitude" = @p4 AND "StorageLocation_Longitude" = @p5;
""");
}
private void AssertSql(params string[] expected)
=> Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
protected override void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction)
=> facade.UseTransaction(transaction.GetDbTransaction());
}
\ No newline at end of file
}
namespace Npgsql.EntityFrameworkCore.PostgreSQL;
public class SerializationNpgsqlTest : SerializationTestBase<F1NpgsqlFixture>
public class SerializationNpgsqlTest : SerializationTestBase<F1BytesNpgsqlFixture>
{
public SerializationNpgsqlTest(F1NpgsqlFixture fixture)
public SerializationNpgsqlTest(F1BytesNpgsqlFixture fixture)
: base(fixture)
{
}
}
\ No newline at end of file
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册