Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rowversion migration regression from EF Core 8 to 9 #35148

Closed
Moerup opened this issue Nov 19, 2024 · 8 comments · Fixed by #35283
Closed

Rowversion migration regression from EF Core 8 to 9 #35148

Moerup opened this issue Nov 19, 2024 · 8 comments · Fixed by #35283

Comments

@Moerup
Copy link

Moerup commented Nov 19, 2024

Rowversion migration regression from EF Core 8 to 9

I'm in the process of upgrading an application from .NET 8 to 9.

But I'm running into problems when trying to run existing migrations that have been created with EF Core 8 and working for over a year now on multiple versions of EF Core (8.0.4 to 8.0.11)

I have a migration that throws exceptions and can't complete at all on v9.0.0, but if I revert to 8.0.11 it works fine.

Include your code

The migration that fails is a rename of a table where we use rowversion configured like this:

public void Configure(EntityTypeBuilder<Sensor> builder)
{
    builder.ToTable(nameof(MasterCatalogueDbContext.Sensors), schema: DatabaseConstants.MasterDataCatalogueSchemaName);

    // Optimistic Concurrency token
    builder.Property(p => p.RowVersion).IsRowVersion();
}

With this migration created for altering the rowversion column:

migrationBuilder.AlterColumn<byte[]>(
    name: "RowVersion",
    schema: "attributeList",
    table: "MeasurementUnits",
    type: "rowversion",
    rowVersion: true,
    nullable: false,
    defaultValue: Array.Empty<byte>(),
    oldClrType: typeof(byte[]),
    oldType: "rowversion",
    oldRowVersion: true,
    oldDefaultValue: Array.Empty<byte>())
    .Annotation("SqlServer:IsTemporal", true)
    .Annotation("SqlServer:TemporalHistoryTableName", "MeasurementUnitsHistory")
    .Annotation("SqlServer:TemporalHistoryTableSchema", "attributeList")
    .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
    .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart")
    .OldAnnotation("SqlServer:IsTemporal", true)
    .OldAnnotation("SqlServer:TemporalHistoryTableName", "SensorUnitsHistory")
    .OldAnnotation("SqlServer:TemporalHistoryTableSchema", "attributeList")
    .OldAnnotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
    .OldAnnotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");

And that throws this error:

2024-11-14T15:17:05 [15:17:05 INF] Applying migration '20240610063634_RenameSensorUnitToMeasurementUnit'.
2024-11-14T15:17:05 [15:17:05 ERR] Failed executing DbCommand (32ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
2024-11-14T15:17:05 DECLARE @var15 sysname;
2024-11-14T15:17:05 SELECT @var15 = [d].[name]
2024-11-14T15:17:05 FROM [sys].[default_constraints] [d]
2024-11-14T15:17:05 INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id]
2024-11-14T15:17:05 WHERE ([d].[parent_object_id] = OBJECT_ID(N'[attributeList].[MeasurementUnits]') AND [c].[name] = N'RowVersion');
2024-11-14T15:17:05 IF @var15 IS NOT NULL EXEC(N'ALTER TABLE [attributeList].[MeasurementUnits] DROP CONSTRAINT [' + @var15 + '];');
2024-11-14T15:17:05 ALTER TABLE [attributeList].[MeasurementUnits] ALTER COLUMN [RowVersion] rowversion NOT NULL;
2024-11-14T15:17:05 ALTER TABLE [attributeList].[MeasurementUnits] ADD DEFAULT 0x FOR [RowVersion];
2024-11-14T15:17:05 [15:17:05 ERR] Failed to migrate MasterCatalogue database!
2024-11-14T15:17:05 Microsoft.Data.SqlClient.SqlException (0x80131904): Cannot alter column 'RowVersion' to be data type timestamp.
2024-11-14T15:17:05    at Microsoft.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
2024-11-14T15:17:05    at Microsoft.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
2024-11-14T15:17:05    at Microsoft.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, Boolean callerHasConnectionLock, Boolean asyncClose)
2024-11-14T15:17:05    at Microsoft.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj, Boolean& dataReady)
2024-11-14T15:17:05    at Microsoft.Data.SqlClient.SqlCommand.RunExecuteNonQueryTds(String methodName, Boolean isAsync, Int32 timeout, Boolean asyncWrite)
2024-11-14T15:17:05    at Microsoft.Data.SqlClient.SqlCommand.InternalExecuteNonQuery(TaskCompletionSource`1 completion, Boolean sendToPipe, Int32 timeout, Boolean& usedCache, Boolean asyncWrite, Boolean inRetry, String methodName)
2024-11-14T15:17:05    at Microsoft.Data.SqlClient.SqlCommand.ExecuteNonQuery()
2024-11-14T15:17:05    at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteNonQuery(RelationalCommandParameterObject parameterObject)
2024-11-14T15:17:05    at Microsoft.EntityFrameworkCore.Migrations.MigrationCommand.ExecuteNonQuery(IRelationalConnection connection, IReadOnlyDictionary`2 parameterValues)
2024-11-14T15:17:05    at Microsoft.EntityFrameworkCore.Migrations.Internal.MigrationCommandExecutor.Execute(IReadOnlyList`1 migrationCommands, IRelationalConnection connection, MigrationExecutionState executionState, Boolean beginTransaction, Boolean commitTransaction, Nullable`1 isolationLevel)
2024-11-14T15:17:05    at Microsoft.EntityFrameworkCore.Migrations.Internal.MigrationCommandExecutor.<>c.<ExecuteNonQuery>b__3_1(DbContext _, ValueTuple`6 s)
2024-11-14T15:17:05    at Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerExecutionStrategy.Execute[TState,TResult](TState state, Func`3 operation, Func`3 verifySucceeded)
2024-11-14T15:17:05    at Microsoft.EntityFrameworkCore.Migrations.Internal.MigrationCommandExecutor.ExecuteNonQuery(IReadOnlyList`1 migrationCommands, IRelationalConnection connection, MigrationExecutionState executionState, Boolean commitTransaction, Nullable`1 isolationLevel)
2024-11-14T15:17:05    at Microsoft.EntityFrameworkCore.Migrations.Internal.Migrator.MigrateImplementation(DbContext context, String targetMigration, MigrationExecutionState state, Boolean useTransaction)
2024-11-14T15:17:05    at Microsoft.EntityFrameworkCore.Migrations.Internal.Migrator.<>c.<Migrate>b__20_1(DbContext c, ValueTuple`4 s)
2024-11-14T15:17:05    at Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerExecutionStrategy.Execute[TState,TResult](TState state, Func`3 operation, Func`3 verifySucceeded)
2024-11-14T15:17:05    at Microsoft.EntityFrameworkCore.Migrations.Internal.Migrator.Migrate(String targetMigration)
2024-11-14T15:17:05    at Microsoft.EntityFrameworkCore.RelationalDatabaseFacadeExtensions.Migrate(DatabaseFacade databaseFacade)
2024-11-14T15:17:05    at RTDT.MasterCatalogue.Web.Program.MigrateAndSeed(WebApplication app) in C:\GitVestas\RTDT\src\Services\RTDT.MasterCatalogue\src\RTDT.MasterCatalogue.Web\Program.cs:line 171
2024-11-14T15:17:05 ClientConnectionId:f4a440da-ec8a-43b2-9051-87cbd87b6faa
2024-11-14T15:17:05 Error Number:4927,State:1,Class:16

Include provider and version information

EF Core version: 9.0.0
Database provider: Microsoft.EntityFrameworkCore.SqlServer
Target framework: .NET 9.0
Operating system: Windows & Linux (Local & CICD)
IDE: Visual Studio Enterprise 2022 17.12.0

@Moerup
Copy link
Author

Moerup commented Nov 19, 2024

Extracted the rowversion issue from this other temporal issue: #35108 based on @roji comment: #35108_

@ajcvickers
Copy link
Contributor

@Moerup I have not been able to reproduce this. Please attach a small, runnable project or post a small, runnable code listing that contains a full model in code that produces the initial migration and then specifically how to change that model to reproduce the migration that you are seeing.

@Moerup
Copy link
Author

Moerup commented Dec 4, 2024

@ajcvickers Have you tried testing it with a table using temporal tables?
I have another issue open, I also mentioned in the other comment (#35108), and maybe they are clashing and it's not related directly to the rowversion.

I sadly don't currently have the resources in the project to spend time on a standalone reproduction of the issues, would love to do it, but the money guys says no, as is tradition 😄

@danroot
Copy link

danroot commented Dec 4, 2024

@ajcvickers I'm not 100% sure, but my repro from the other ticket may help here also. The issue there is a migration created by EFCore 8 that is then applied by EFCore 9. In that case, we have unit tests that recreate the database using migrations. These tests fail when switching to EFCore 9, running EFCore 8-generated migration for a temporal table. I suspect similar in this case, and can try to fork the repository to prove it out. In general:

  • Create a project with EF Core 8 db context
  • Add an initial migration
  • Update the tables to use RowVersion in OnModelCreating of the db context: builder.Property(p => p.RowVersion).IsRowVersion();
  • Add a migration for the change
  • Update the Microsoft.EntityFrameworkCore.* nuget packages to to EF Core 9
  • Run the migration by calling context.Database.Migrate or using dotnet ef database update

@danroot
Copy link

danroot commented Dec 4, 2024

Full repro of this error. It's worth noting I could not get it to repro if I just had RowVersion migration. The issue is a combination of RowVersion+TemporalTable migrations generated from EFCore8, when run by EFCore9 causes the error "Cannot alter column 'RowVersion' to be data type timestamp". This is a different error from the other ticket, but I suspect being caused by the same area of EFCore9 code. I suspect these changes, but haven't dove into the EFCore code to try to figure out further.

In our case, the related issue is blocking EFCore9 adoption. Possible workarounds include modifying the migration cs somehow or complete reset of migrations.

@Moerup
Copy link
Author

Moerup commented Dec 4, 2024

Thanks a lot @danroot for taking the time to make repros of not only 1, but 2 of my created issues!

maumar added a commit that referenced this issue Dec 6, 2024
In 9 we changed the way we process migration of temporal tables. One of the changes was drastically reducing the number of annotations for columns which are part of temporal tables.
This however caused regressions for cases where migration code was created using EF8 (and containing those legacy annotations) but then executed using EF9 tooling.
Specifically, extra annotations were generating a number of superfluous Alter Column operations (which were only modifying those annotations). In EF8 we had logic to weed out those operations, but it was removed in EF9.

Fix is to remove all the legacy annotations on column operations before we start processing them. We no longer rely on them, but rather use annotations on Table operations and/or relational model.
The only exception is CreateColumnOperation, so for it we convert old annotations to TemporalIsPeriodStartColumn and TemporalIsPeriodEndColumn where appropriate.
Also, we are bringing back logic from EF8 which removed unnecessary AlterColumnOperations if the old and new columns are the same after the legacy temporal annotations have been removed.

Also fixes #35148 which was the same underlying issue.
@maumar maumar added the closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. label Dec 6, 2024
maumar added a commit that referenced this issue Dec 6, 2024
In 9 we changed the way we process migration of temporal tables. One of the changes was drastically reducing the number of annotations for columns which are part of temporal tables.
This however caused regressions for cases where migration code was created using EF8 (and containing those legacy annotations) but then executed using EF9 tooling.
Specifically, extra annotations were generating a number of superfluous Alter Column operations (which were only modifying those annotations). In EF8 we had logic to weed out those operations, but it was removed in EF9.

Fix is to remove all the legacy annotations on column operations before we start processing them. We no longer rely on them, but rather use annotations on Table operations and/or relational model.
The only exception is CreateColumnOperation, so for it we convert old annotations to TemporalIsPeriodStartColumn and TemporalIsPeriodEndColumn where appropriate.
Also, we are bringing back logic from EF8 which removed unnecessary AlterColumnOperations if the old and new columns are the same after the legacy temporal annotations have been removed.

Fixes #35108
Also fixes #35148 which was the same underlying issue.
maumar added a commit that referenced this issue Dec 7, 2024
In 9 we changed the way we process migration of temporal tables. One of the changes was drastically reducing the number of annotations for columns which are part of temporal tables.
This however caused regressions for cases where migration code was created using EF8 (and containing those legacy annotations) but then executed using EF9 tooling.
Specifically, extra annotations were generating a number of superfluous Alter Column operations (which were only modifying those annotations). In EF8 we had logic to weed out those operations, but it was removed in EF9.

Fix is to remove all the legacy annotations on column operations before we start processing them. We no longer rely on them, but rather use annotations on Table operations and/or relational model.
The only exception is CreateColumnOperation, so for it we convert old annotations to TemporalIsPeriodStartColumn and TemporalIsPeriodEndColumn where appropriate.
Also, we are bringing back logic from EF8 which removed unnecessary AlterColumnOperations if the old and new columns are the same after the legacy temporal annotations have been removed.

Fixes #35108
Also fixes #35148 which was the same underlying issue.
@maumar maumar closed this as completed in 8a918fc Dec 7, 2024
@killnine
Copy link

killnine commented Jan 6, 2025

Thanks for the legwok @danroot, this regression hit my project too as I was updating to .NET 9. I have the exact situation where my solution originally did not have temporal tables applied to entities but they were added later. Now my CI pipeline fails with the error about

Cannot alter column 'Version' to be data type timestamp

Pretty tricky and will prevent this update from going to production. So does a fix look to be heading for 9.0.2?

@danroot
Copy link

danroot commented Jan 7, 2025

Yes, my understanding is this is fixed for 9.0.2 in #35289

If you need fix before that, @maumar 's fix here works.. I even looked into writing a script to fixup ours in the way he suggests (we have dozens of migrations with this issue), but couldn't quite get a script I was comfortable with.

An alternative, IF you don't have to support any old-version databases, would be to squash migrations. and start over. That makes most sense in like an enterprise environment where you know all dev, test, and production instances of the db are up-to-date.

@maumar maumar marked this as a duplicate of #35611 Feb 10, 2025
@maumar maumar added closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. closed-duplicate and removed closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. labels Feb 10, 2025
@maumar maumar closed this as completed Feb 10, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants