From 32293b1155646eb38debc46657757e6702a7f1ab Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 5 Sep 2025 20:57:44 +0000
Subject: [PATCH 01/39] Initial plan
From fa6fbfe1654e2194993346211a4791e431295ab6 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 5 Sep 2025 21:13:59 +0000
Subject: [PATCH 02/39] Implement first-class SQLite AUTOINCREMENT support
Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
---
.../SqlitePropertyBuilderExtensions.cs | 59 +++++++++++++
.../Extensions/SqlitePropertyExtensions.cs | 84 +++++++++++++++++++
.../SqliteServiceCollectionExtensions.cs | 1 +
.../Conventions/SqliteConventionSetBuilder.cs | 1 +
.../SqliteValueGenerationConvention.cs | 68 +++++++++++++++
.../Internal/SqliteAnnotationNames.cs | 8 ++
.../Internal/SqliteAnnotationProvider.cs | 13 +--
.../Metadata/SqliteValueGenerationStrategy.cs | 31 +++++++
.../SqliteMigrationsAnnotationProvider.cs | 46 ++++++++++
9 files changed, 301 insertions(+), 10 deletions(-)
create mode 100644 src/EFCore.Sqlite.Core/Metadata/Conventions/SqliteValueGenerationConvention.cs
create mode 100644 src/EFCore.Sqlite.Core/Metadata/SqliteValueGenerationStrategy.cs
create mode 100644 src/EFCore.Sqlite.Core/Migrations/Internal/SqliteMigrationsAnnotationProvider.cs
diff --git a/src/EFCore.Sqlite.Core/Extensions/SqlitePropertyBuilderExtensions.cs b/src/EFCore.Sqlite.Core/Extensions/SqlitePropertyBuilderExtensions.cs
index a70174eddec..bc3f9e73555 100644
--- a/src/EFCore.Sqlite.Core/Extensions/SqlitePropertyBuilderExtensions.cs
+++ b/src/EFCore.Sqlite.Core/Extensions/SqlitePropertyBuilderExtensions.cs
@@ -15,6 +15,65 @@ namespace Microsoft.EntityFrameworkCore;
///
public static class SqlitePropertyBuilderExtensions
{
+ ///
+ /// Configures the property to use SQLite AUTOINCREMENT feature to generate values for new entities,
+ /// when targeting SQLite. This method sets the property to be .
+ ///
+ ///
+ /// AUTOINCREMENT can only be used on integer primary key columns in SQLite.
+ /// See Modeling entity types and relationships, and
+ /// Accessing SQLite databases with EF Core for more information and examples.
+ ///
+ /// The builder for the property being configured.
+ /// The same builder instance so that multiple calls can be chained.
+ public static PropertyBuilder UseAutoincrement(this PropertyBuilder propertyBuilder)
+ {
+ var property = propertyBuilder.Metadata;
+ property.SetValueGenerationStrategy(SqliteValueGenerationStrategy.Autoincrement);
+
+ return propertyBuilder;
+ }
+
+ ///
+ /// Configures the property to use SQLite AUTOINCREMENT feature to generate values for new entities,
+ /// when targeting SQLite. This method sets the property to be .
+ ///
+ ///
+ /// AUTOINCREMENT can only be used on integer primary key columns in SQLite.
+ /// See Modeling entity types and relationships, and
+ /// Accessing SQLite databases with EF Core for more information and examples.
+ ///
+ /// The type of the property being configured.
+ /// The builder for the property being configured.
+ /// The same builder instance so that multiple calls can be chained.
+ public static PropertyBuilder UseAutoincrement(
+ this PropertyBuilder propertyBuilder)
+ => (PropertyBuilder)UseAutoincrement((PropertyBuilder)propertyBuilder);
+
+ ///
+ /// Configures the value generation strategy for the property when targeting SQLite.
+ ///
+ /// The builder for the property being configured.
+ /// The strategy to use.
+ /// Indicates whether the configuration was specified using a data annotation.
+ ///
+ /// The same builder instance if the configuration was applied,
+ /// otherwise.
+ ///
+ public static IConventionPropertyBuilder? HasValueGenerationStrategy(
+ this IConventionPropertyBuilder propertyBuilder,
+ SqliteValueGenerationStrategy? strategy,
+ bool fromDataAnnotation = false)
+ {
+ if (propertyBuilder.CanSetAnnotation(
+ SqliteAnnotationNames.ValueGenerationStrategy, strategy, fromDataAnnotation))
+ {
+ propertyBuilder.Metadata.SetValueGenerationStrategy(strategy, fromDataAnnotation);
+ return propertyBuilder;
+ }
+
+ return null;
+ }
///
/// Configures the SRID of the column that the property maps to when targeting SQLite.
///
diff --git a/src/EFCore.Sqlite.Core/Extensions/SqlitePropertyExtensions.cs b/src/EFCore.Sqlite.Core/Extensions/SqlitePropertyExtensions.cs
index bcf0b3815fb..e76bc8961ce 100644
--- a/src/EFCore.Sqlite.Core/Extensions/SqlitePropertyExtensions.cs
+++ b/src/EFCore.Sqlite.Core/Extensions/SqlitePropertyExtensions.cs
@@ -16,6 +16,90 @@ namespace Microsoft.EntityFrameworkCore;
public static class SqlitePropertyExtensions
{
///
+ /// Returns the to use for the property.
+ ///
+ /// The property.
+ /// The strategy to use for the property.
+ public static SqliteValueGenerationStrategy GetValueGenerationStrategy(this IReadOnlyProperty property)
+ {
+ var annotation = property[SqliteAnnotationNames.ValueGenerationStrategy];
+ if (annotation != null)
+ {
+ return (SqliteValueGenerationStrategy)annotation;
+ }
+
+ return GetDefaultValueGenerationStrategy(property);
+ }
+
+ ///
+ /// Returns the to use for the property.
+ ///
+ /// The property.
+ /// The identifier of the store object.
+ /// The strategy to use for the property.
+ public static SqliteValueGenerationStrategy GetValueGenerationStrategy(
+ this IReadOnlyProperty property,
+ in StoreObjectIdentifier storeObject)
+ {
+ var annotation = property.FindAnnotation(SqliteAnnotationNames.ValueGenerationStrategy);
+ if (annotation != null)
+ {
+ return (SqliteValueGenerationStrategy)annotation.Value!;
+ }
+
+ var sharedProperty = property.FindSharedStoreObjectRootProperty(storeObject);
+ return sharedProperty != null
+ ? sharedProperty.GetValueGenerationStrategy(storeObject)
+ : GetDefaultValueGenerationStrategy(property);
+ }
+
+ private static SqliteValueGenerationStrategy GetDefaultValueGenerationStrategy(IReadOnlyProperty property)
+ {
+ var primaryKey = property.DeclaringType.ContainingEntityType.FindPrimaryKey();
+ return primaryKey is { Properties.Count: 1 }
+ && primaryKey.Properties[0] == property
+ && property.ValueGenerated == ValueGenerated.OnAdd
+ && property.ClrType.UnwrapNullableType().IsInteger()
+ && property.FindTypeMapping()?.Converter == null
+ ? SqliteValueGenerationStrategy.Autoincrement
+ : SqliteValueGenerationStrategy.None;
+ }
+
+ ///
+ /// Sets the to use for the property.
+ ///
+ /// The property.
+ /// The strategy to use.
+ public static void SetValueGenerationStrategy(
+ this IMutableProperty property,
+ SqliteValueGenerationStrategy? value)
+ => property.SetOrRemoveAnnotation(SqliteAnnotationNames.ValueGenerationStrategy, value);
+
+ ///
+ /// Sets the to use for the property.
+ ///
+ /// The property.
+ /// The strategy to use.
+ /// Indicates whether the configuration was specified using a data annotation.
+ /// The configured value.
+ public static SqliteValueGenerationStrategy? SetValueGenerationStrategy(
+ this IConventionProperty property,
+ SqliteValueGenerationStrategy? value,
+ bool fromDataAnnotation = false)
+ => (SqliteValueGenerationStrategy?)property.SetOrRemoveAnnotation(
+ SqliteAnnotationNames.ValueGenerationStrategy, value, fromDataAnnotation)?.Value;
+
+ ///
+ /// Gets the for the value generation strategy.
+ ///
+ /// The property.
+ /// The for the value generation strategy.
+ public static ConfigurationSource? GetValueGenerationStrategyConfigurationSource(this IConventionProperty property)
+ => property.FindAnnotation(SqliteAnnotationNames.ValueGenerationStrategy)?.GetConfigurationSource();
+
+ private static bool HasConverter(IProperty property)
+ => property.FindTypeMapping()?.Converter != null;
+ ///
/// Returns the SRID to use when creating a column for this property.
///
/// The property.
diff --git a/src/EFCore.Sqlite.Core/Extensions/SqliteServiceCollectionExtensions.cs b/src/EFCore.Sqlite.Core/Extensions/SqliteServiceCollectionExtensions.cs
index c4c6f1680ce..50bcb6e07b3 100644
--- a/src/EFCore.Sqlite.Core/Extensions/SqliteServiceCollectionExtensions.cs
+++ b/src/EFCore.Sqlite.Core/Extensions/SqliteServiceCollectionExtensions.cs
@@ -95,6 +95,7 @@ public static IServiceCollection AddEntityFrameworkSqlite(this IServiceCollectio
.TryAdd()
.TryAdd()
.TryAdd()
+ .TryAdd()
.TryAdd()
.TryAdd()
.TryAdd()
diff --git a/src/EFCore.Sqlite.Core/Metadata/Conventions/SqliteConventionSetBuilder.cs b/src/EFCore.Sqlite.Core/Metadata/Conventions/SqliteConventionSetBuilder.cs
index 1cebd484d5b..b37b2c6a407 100644
--- a/src/EFCore.Sqlite.Core/Metadata/Conventions/SqliteConventionSetBuilder.cs
+++ b/src/EFCore.Sqlite.Core/Metadata/Conventions/SqliteConventionSetBuilder.cs
@@ -45,6 +45,7 @@ public override ConventionSet CreateConventionSet()
conventionSet.Replace(new SqliteSharedTableConvention(Dependencies, RelationalDependencies));
conventionSet.Replace(new SqliteRuntimeModelConvention(Dependencies, RelationalDependencies));
+ conventionSet.Replace(new SqliteValueGenerationConvention(Dependencies, RelationalDependencies));
return conventionSet;
}
diff --git a/src/EFCore.Sqlite.Core/Metadata/Conventions/SqliteValueGenerationConvention.cs b/src/EFCore.Sqlite.Core/Metadata/Conventions/SqliteValueGenerationConvention.cs
new file mode 100644
index 00000000000..0e208a8566a
--- /dev/null
+++ b/src/EFCore.Sqlite.Core/Metadata/Conventions/SqliteValueGenerationConvention.cs
@@ -0,0 +1,68 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+// ReSharper disable once CheckNamespace
+
+namespace Microsoft.EntityFrameworkCore.Metadata.Conventions;
+
+///
+/// A convention that configures the SQLite value generation strategy for properties.
+///
+///
+/// See Model building conventions, and
+/// Accessing SQLite databases with EF Core
+/// for more information and examples.
+///
+public class SqliteValueGenerationConvention : ValueGenerationConvention
+{
+ ///
+ /// Creates a new instance of .
+ ///
+ /// Parameter object containing dependencies for this convention.
+ /// Parameter object containing relational dependencies for this convention.
+ public SqliteValueGenerationConvention(
+ ProviderConventionSetBuilderDependencies dependencies,
+ RelationalConventionSetBuilderDependencies relationalDependencies)
+ : base(dependencies)
+ {
+ RelationalDependencies = relationalDependencies;
+ }
+
+ ///
+ /// Relational provider-specific dependencies for this service.
+ ///
+ protected virtual RelationalConventionSetBuilderDependencies RelationalDependencies { get; }
+
+ ///
+ /// Returns the store value generation strategy to set for the given property.
+ ///
+ /// The property.
+ /// The strategy to set for the property.
+ protected override ValueGenerated? GetValueGenerated(IConventionProperty property)
+ {
+ var declaringType = property.DeclaringType;
+
+ var strategy = GetValueGenerationStrategy(property);
+ if (strategy == SqliteValueGenerationStrategy.Autoincrement)
+ {
+ return ValueGenerated.OnAdd;
+ }
+
+ return base.GetValueGenerated(property);
+ }
+
+ private static SqliteValueGenerationStrategy GetValueGenerationStrategy(IConventionProperty property)
+ {
+ var entityType = (IConventionEntityType)property.DeclaringType;
+ var primaryKey = entityType.FindPrimaryKey();
+ if (primaryKey is { Properties.Count: 1 }
+ && primaryKey.Properties[0] == property
+ && property.ClrType.UnwrapNullableType().IsInteger()
+ && property.FindTypeMapping()?.Converter == null)
+ {
+ return SqliteValueGenerationStrategy.Autoincrement;
+ }
+
+ return SqliteValueGenerationStrategy.None;
+ }
+}
\ No newline at end of file
diff --git a/src/EFCore.Sqlite.Core/Metadata/Internal/SqliteAnnotationNames.cs b/src/EFCore.Sqlite.Core/Metadata/Internal/SqliteAnnotationNames.cs
index 6e74bb68d4e..e6c7b56b181 100644
--- a/src/EFCore.Sqlite.Core/Metadata/Internal/SqliteAnnotationNames.cs
+++ b/src/EFCore.Sqlite.Core/Metadata/Internal/SqliteAnnotationNames.cs
@@ -74,4 +74,12 @@ public static class SqliteAnnotationNames
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
public const string UseSqlReturningClause = Prefix + "UseSqlReturningClause";
+
+ ///
+ /// 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.
+ ///
+ public const string ValueGenerationStrategy = Prefix + "ValueGenerationStrategy";
}
diff --git a/src/EFCore.Sqlite.Core/Metadata/Internal/SqliteAnnotationProvider.cs b/src/EFCore.Sqlite.Core/Metadata/Internal/SqliteAnnotationProvider.cs
index 525780bb16a..9bbe831df1b 100644
--- a/src/EFCore.Sqlite.Core/Metadata/Internal/SqliteAnnotationProvider.cs
+++ b/src/EFCore.Sqlite.Core/Metadata/Internal/SqliteAnnotationProvider.cs
@@ -65,13 +65,9 @@ public override IEnumerable For(IColumn column, bool designTime)
// Model validation ensures that these facets are the same on all mapped properties
var property = column.PropertyMappings.First().Property;
- // Only return auto increment for integer single column primary key
- var primaryKey = property.DeclaringType.ContainingEntityType.FindPrimaryKey();
- if (primaryKey is { Properties.Count: 1 }
- && primaryKey.Properties[0] == property
- && property.ValueGenerated == ValueGenerated.OnAdd
- && property.ClrType.UnwrapNullableType().IsInteger()
- && !HasConverter(property))
+
+ // Use the strategy-based approach to determine AUTOINCREMENT
+ if (property.GetValueGenerationStrategy() == SqliteValueGenerationStrategy.Autoincrement)
{
yield return new Annotation(SqliteAnnotationNames.Autoincrement, true);
}
@@ -82,7 +78,4 @@ public override IEnumerable For(IColumn column, bool designTime)
yield return new Annotation(SqliteAnnotationNames.Srid, srid);
}
}
-
- private static bool HasConverter(IProperty property)
- => property.FindTypeMapping()?.Converter != null;
}
diff --git a/src/EFCore.Sqlite.Core/Metadata/SqliteValueGenerationStrategy.cs b/src/EFCore.Sqlite.Core/Metadata/SqliteValueGenerationStrategy.cs
new file mode 100644
index 00000000000..147fd163ca3
--- /dev/null
+++ b/src/EFCore.Sqlite.Core/Metadata/SqliteValueGenerationStrategy.cs
@@ -0,0 +1,31 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+// ReSharper disable once CheckNamespace
+
+namespace Microsoft.EntityFrameworkCore.Metadata;
+
+///
+/// Defines strategies to use across the EF Core stack when generating key values
+/// from SQLite database columns.
+///
+///
+/// See Model building conventions, and
+/// Accessing SQLite databases with EF Core
+/// for more information and examples.
+///
+public enum SqliteValueGenerationStrategy
+{
+ ///
+ /// No SQLite-specific strategy
+ ///
+ None,
+
+ ///
+ /// A pattern that uses SQLite's AUTOINCREMENT feature to generate values for new entities.
+ ///
+ ///
+ /// AUTOINCREMENT can only be used on integer primary key columns in SQLite.
+ ///
+ Autoincrement
+}
\ No newline at end of file
diff --git a/src/EFCore.Sqlite.Core/Migrations/Internal/SqliteMigrationsAnnotationProvider.cs b/src/EFCore.Sqlite.Core/Migrations/Internal/SqliteMigrationsAnnotationProvider.cs
new file mode 100644
index 00000000000..e5ba15c99cc
--- /dev/null
+++ b/src/EFCore.Sqlite.Core/Migrations/Internal/SqliteMigrationsAnnotationProvider.cs
@@ -0,0 +1,46 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.EntityFrameworkCore.Sqlite.Metadata.Internal;
+
+namespace Microsoft.EntityFrameworkCore.Sqlite.Migrations.Internal;
+
+///
+/// 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.
+///
+public class SqliteMigrationsAnnotationProvider : MigrationsAnnotationProvider
+{
+ ///
+ /// Initializes a new instance of this class.
+ ///
+ /// Parameter object containing dependencies for this service.
+#pragma warning disable EF1001 // Internal EF Core API usage.
+ public SqliteMigrationsAnnotationProvider(MigrationsAnnotationProviderDependencies dependencies)
+#pragma warning restore EF1001 // Internal EF Core API usage.
+ : base(dependencies)
+ {
+ }
+
+ ///
+ public override IEnumerable ForRemove(IColumn column)
+ {
+ // Preserve the autoincrement annotation when removing columns for SQLite migrations
+ if (column[SqliteAnnotationNames.Autoincrement] as bool? == true)
+ {
+ yield return new Annotation(SqliteAnnotationNames.Autoincrement, true);
+ }
+ }
+
+ ///
+ public override IEnumerable ForRename(IColumn column)
+ {
+ // Preserve the autoincrement annotation when renaming columns for SQLite migrations
+ if (column[SqliteAnnotationNames.Autoincrement] as bool? == true)
+ {
+ yield return new Annotation(SqliteAnnotationNames.Autoincrement, true);
+ }
+ }
+}
\ No newline at end of file
From a9b7973e4feadee712308bdf3e5f1e1c8dd5373e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 5 Sep 2025 21:24:09 +0000
Subject: [PATCH 03/39] Add comprehensive tests for SQLite AUTOINCREMENT
first-class support
Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
---
.../SqliteAutoincrementIntegrationTest.cs | 84 +++++++++++
.../SqliteValueGenerationStrategyTest.cs | 138 ++++++++++++++++++
2 files changed, 222 insertions(+)
create mode 100644 test/EFCore.Sqlite.Tests/Extensions/SqliteAutoincrementIntegrationTest.cs
create mode 100644 test/EFCore.Sqlite.Tests/Extensions/SqliteValueGenerationStrategyTest.cs
diff --git a/test/EFCore.Sqlite.Tests/Extensions/SqliteAutoincrementIntegrationTest.cs b/test/EFCore.Sqlite.Tests/Extensions/SqliteAutoincrementIntegrationTest.cs
new file mode 100644
index 00000000000..96040bcc743
--- /dev/null
+++ b/test/EFCore.Sqlite.Tests/Extensions/SqliteAutoincrementIntegrationTest.cs
@@ -0,0 +1,84 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.EntityFrameworkCore.Sqlite.Internal;
+
+namespace Microsoft.EntityFrameworkCore;
+
+public class SqliteAutoincrementIntegrationTest : IDisposable
+{
+ private readonly DbContext _context;
+
+ public SqliteAutoincrementIntegrationTest()
+ {
+ var serviceProvider = new ServiceCollection()
+ .AddEntityFrameworkSqlite()
+ .BuildServiceProvider();
+
+ var optionsBuilder = new DbContextOptionsBuilder()
+ .UseSqlite("Data Source=:memory:")
+ .UseInternalServiceProvider(serviceProvider);
+
+ _context = new TestContext(optionsBuilder.Options);
+ _context.Database.EnsureCreated();
+ }
+
+ [ConditionalFact]
+ public void UseAutoincrement_configures_column_correctly()
+ {
+ // Verify that the UseAutoincrement method produces the correct SQL
+ var sql = _context.Database.GenerateCreateScript();
+
+ // Should contain AUTOINCREMENT for the Id column
+ Assert.Contains("AUTOINCREMENT", sql);
+ Assert.Contains("\"Id\" INTEGER NOT NULL", sql);
+ }
+
+ [ConditionalFact]
+ public void Autoincrement_works_for_inserts()
+ {
+ // Insert a record without specifying the ID
+ var customer = new Customer { Name = "Test Customer" };
+ ((TestContext)_context).Customers.Add(customer);
+ _context.SaveChanges();
+
+ // The ID should be automatically generated
+ Assert.True(customer.Id > 0);
+ }
+
+ [ConditionalFact]
+ public void Value_generation_strategy_is_preserved_in_model()
+ {
+ var entityType = _context.Model.FindEntityType(typeof(Customer))!;
+ var idProperty = entityType.FindProperty(nameof(Customer.Id))!;
+
+ Assert.Equal(SqliteValueGenerationStrategy.Autoincrement, idProperty.GetValueGenerationStrategy());
+ Assert.Equal(ValueGenerated.OnAdd, idProperty.ValueGenerated);
+ }
+
+ public void Dispose()
+ {
+ _context?.Dispose();
+ }
+
+ private class TestContext : DbContext
+ {
+ public TestContext(DbContextOptions options) : base(options) { }
+
+ public DbSet Customers { get; set; } = null!;
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ modelBuilder.Entity(b =>
+ {
+ b.Property(e => e.Id).UseAutoincrement();
+ });
+ }
+ }
+
+ private class Customer
+ {
+ public int Id { get; set; }
+ public string? Name { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/test/EFCore.Sqlite.Tests/Extensions/SqliteValueGenerationStrategyTest.cs b/test/EFCore.Sqlite.Tests/Extensions/SqliteValueGenerationStrategyTest.cs
new file mode 100644
index 00000000000..82374952500
--- /dev/null
+++ b/test/EFCore.Sqlite.Tests/Extensions/SqliteValueGenerationStrategyTest.cs
@@ -0,0 +1,138 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.EntityFrameworkCore;
+
+public class SqliteValueGenerationStrategyTest
+{
+ [ConditionalFact]
+ public void Can_get_and_set_value_generation_strategy()
+ {
+ var modelBuilder = new ModelBuilder();
+
+ var property = modelBuilder
+ .Entity()
+ .Property(e => e.Id)
+ .Metadata;
+
+ Assert.Equal(SqliteValueGenerationStrategy.None, property.GetValueGenerationStrategy());
+
+ property.SetValueGenerationStrategy(SqliteValueGenerationStrategy.Autoincrement);
+
+ Assert.Equal(SqliteValueGenerationStrategy.Autoincrement, property.GetValueGenerationStrategy());
+
+ property.SetValueGenerationStrategy(null);
+
+ Assert.Equal(SqliteValueGenerationStrategy.None, property.GetValueGenerationStrategy());
+ }
+
+ [ConditionalFact]
+ public void UseAutoincrement_sets_value_generation_strategy()
+ {
+ var modelBuilder = new ModelBuilder();
+
+ var propertyBuilder = modelBuilder
+ .Entity()
+ .Property(e => e.Id);
+
+ propertyBuilder.UseAutoincrement();
+
+ Assert.Equal(SqliteValueGenerationStrategy.Autoincrement, propertyBuilder.Metadata.GetValueGenerationStrategy());
+ }
+
+ [ConditionalFact]
+ public void Generic_UseAutoincrement_sets_value_generation_strategy()
+ {
+ var modelBuilder = new ModelBuilder();
+
+ var propertyBuilder = modelBuilder
+ .Entity()
+ .Property(e => e.Id);
+
+ propertyBuilder.UseAutoincrement();
+
+ Assert.Equal(SqliteValueGenerationStrategy.Autoincrement, propertyBuilder.Metadata.GetValueGenerationStrategy());
+ }
+
+ [ConditionalFact]
+ public void Default_value_generation_strategy_for_integer_primary_key()
+ {
+ var modelBuilder = new ModelBuilder();
+
+ var property = modelBuilder
+ .Entity()
+ .Property(e => e.Id)
+ .Metadata;
+
+ // Without conventions, the default should be None
+ Assert.Equal(SqliteValueGenerationStrategy.None, property.GetValueGenerationStrategy());
+ }
+
+ [ConditionalFact]
+ public void No_autoincrement_for_non_primary_key()
+ {
+ var modelBuilder = new ModelBuilder();
+
+ var property = modelBuilder
+ .Entity()
+ .Property(e => e.OtherId)
+ .Metadata;
+
+ Assert.Equal(SqliteValueGenerationStrategy.None, property.GetValueGenerationStrategy());
+ Assert.Equal(ValueGenerated.Never, property.ValueGenerated);
+ }
+
+ [ConditionalFact]
+ public void No_autoincrement_for_non_integer_primary_key()
+ {
+ var modelBuilder = new ModelBuilder();
+
+ var property = modelBuilder
+ .Entity()
+ .Property(e => e.Id)
+ .Metadata;
+
+ Assert.Equal(SqliteValueGenerationStrategy.None, property.GetValueGenerationStrategy());
+ Assert.Equal(ValueGenerated.Never, property.ValueGenerated);
+ }
+
+ [ConditionalFact]
+ public void No_autoincrement_for_composite_primary_key()
+ {
+ var modelBuilder = new ModelBuilder();
+
+ modelBuilder
+ .Entity(b =>
+ {
+ b.HasKey(e => new { e.Id1, e.Id2 });
+ });
+
+ var property1 = modelBuilder.Entity().Property(e => e.Id1).Metadata;
+ var property2 = modelBuilder.Entity().Property(e => e.Id2).Metadata;
+
+ Assert.Equal(SqliteValueGenerationStrategy.None, property1.GetValueGenerationStrategy());
+ Assert.Equal(SqliteValueGenerationStrategy.None, property2.GetValueGenerationStrategy());
+ Assert.Equal(ValueGenerated.Never, property1.ValueGenerated);
+ Assert.Equal(ValueGenerated.Never, property2.ValueGenerated);
+ }
+
+ private class Customer
+ {
+ public int Id { get; set; }
+ public int OtherId { get; set; }
+ public string? Name { get; set; }
+ }
+
+ private class CustomerWithStringKey
+ {
+ public string Id { get; set; } = null!;
+ public string? Name { get; set; }
+ }
+
+ private class CustomerWithCompositeKey
+ {
+ public int Id1 { get; set; }
+ public int Id2 { get; set; }
+ public string? Name { get; set; }
+ }
+}
\ No newline at end of file
From 738d27fac5166650e62b3e6dd8368e81d3bf6729 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 5 Sep 2025 21:25:45 +0000
Subject: [PATCH 04/39] Finalize SQLite AUTOINCREMENT first-class
implementation
Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
---
.../SqliteAutoincrementIntegrationTest.cs | 84 -------------------
1 file changed, 84 deletions(-)
delete mode 100644 test/EFCore.Sqlite.Tests/Extensions/SqliteAutoincrementIntegrationTest.cs
diff --git a/test/EFCore.Sqlite.Tests/Extensions/SqliteAutoincrementIntegrationTest.cs b/test/EFCore.Sqlite.Tests/Extensions/SqliteAutoincrementIntegrationTest.cs
deleted file mode 100644
index 96040bcc743..00000000000
--- a/test/EFCore.Sqlite.Tests/Extensions/SqliteAutoincrementIntegrationTest.cs
+++ /dev/null
@@ -1,84 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using Microsoft.EntityFrameworkCore.Sqlite.Internal;
-
-namespace Microsoft.EntityFrameworkCore;
-
-public class SqliteAutoincrementIntegrationTest : IDisposable
-{
- private readonly DbContext _context;
-
- public SqliteAutoincrementIntegrationTest()
- {
- var serviceProvider = new ServiceCollection()
- .AddEntityFrameworkSqlite()
- .BuildServiceProvider();
-
- var optionsBuilder = new DbContextOptionsBuilder()
- .UseSqlite("Data Source=:memory:")
- .UseInternalServiceProvider(serviceProvider);
-
- _context = new TestContext(optionsBuilder.Options);
- _context.Database.EnsureCreated();
- }
-
- [ConditionalFact]
- public void UseAutoincrement_configures_column_correctly()
- {
- // Verify that the UseAutoincrement method produces the correct SQL
- var sql = _context.Database.GenerateCreateScript();
-
- // Should contain AUTOINCREMENT for the Id column
- Assert.Contains("AUTOINCREMENT", sql);
- Assert.Contains("\"Id\" INTEGER NOT NULL", sql);
- }
-
- [ConditionalFact]
- public void Autoincrement_works_for_inserts()
- {
- // Insert a record without specifying the ID
- var customer = new Customer { Name = "Test Customer" };
- ((TestContext)_context).Customers.Add(customer);
- _context.SaveChanges();
-
- // The ID should be automatically generated
- Assert.True(customer.Id > 0);
- }
-
- [ConditionalFact]
- public void Value_generation_strategy_is_preserved_in_model()
- {
- var entityType = _context.Model.FindEntityType(typeof(Customer))!;
- var idProperty = entityType.FindProperty(nameof(Customer.Id))!;
-
- Assert.Equal(SqliteValueGenerationStrategy.Autoincrement, idProperty.GetValueGenerationStrategy());
- Assert.Equal(ValueGenerated.OnAdd, idProperty.ValueGenerated);
- }
-
- public void Dispose()
- {
- _context?.Dispose();
- }
-
- private class TestContext : DbContext
- {
- public TestContext(DbContextOptions options) : base(options) { }
-
- public DbSet Customers { get; set; } = null!;
-
- protected override void OnModelCreating(ModelBuilder modelBuilder)
- {
- modelBuilder.Entity(b =>
- {
- b.Property(e => e.Id).UseAutoincrement();
- });
- }
- }
-
- private class Customer
- {
- public int Id { get; set; }
- public string? Name { get; set; }
- }
-}
\ No newline at end of file
From a1ab3b06871e679803e7a64f499feb9d9f69017a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 5 Sep 2025 21:39:39 +0000
Subject: [PATCH 05/39] Fix SQLite AUTOINCREMENT to work with value converters
for issues #30699 and #29519
Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
---
.../Extensions/SqlitePropertyExtensions.cs | 4 +-
.../SqliteValueGenerationConvention.cs | 3 +-
.../SqliteAutoincrementWithConverterTest.cs | 143 ++++++++++++++++++
3 files changed, 145 insertions(+), 5 deletions(-)
create mode 100644 test/EFCore.Sqlite.FunctionalTests/SqliteAutoincrementWithConverterTest.cs
diff --git a/src/EFCore.Sqlite.Core/Extensions/SqlitePropertyExtensions.cs b/src/EFCore.Sqlite.Core/Extensions/SqlitePropertyExtensions.cs
index e76bc8961ce..15bfe9b749c 100644
--- a/src/EFCore.Sqlite.Core/Extensions/SqlitePropertyExtensions.cs
+++ b/src/EFCore.Sqlite.Core/Extensions/SqlitePropertyExtensions.cs
@@ -60,7 +60,6 @@ private static SqliteValueGenerationStrategy GetDefaultValueGenerationStrategy(I
&& primaryKey.Properties[0] == property
&& property.ValueGenerated == ValueGenerated.OnAdd
&& property.ClrType.UnwrapNullableType().IsInteger()
- && property.FindTypeMapping()?.Converter == null
? SqliteValueGenerationStrategy.Autoincrement
: SqliteValueGenerationStrategy.None;
}
@@ -97,8 +96,7 @@ public static void SetValueGenerationStrategy(
public static ConfigurationSource? GetValueGenerationStrategyConfigurationSource(this IConventionProperty property)
=> property.FindAnnotation(SqliteAnnotationNames.ValueGenerationStrategy)?.GetConfigurationSource();
- private static bool HasConverter(IProperty property)
- => property.FindTypeMapping()?.Converter != null;
+
///
/// Returns the SRID to use when creating a column for this property.
///
diff --git a/src/EFCore.Sqlite.Core/Metadata/Conventions/SqliteValueGenerationConvention.cs b/src/EFCore.Sqlite.Core/Metadata/Conventions/SqliteValueGenerationConvention.cs
index 0e208a8566a..a2c05c26aa1 100644
--- a/src/EFCore.Sqlite.Core/Metadata/Conventions/SqliteValueGenerationConvention.cs
+++ b/src/EFCore.Sqlite.Core/Metadata/Conventions/SqliteValueGenerationConvention.cs
@@ -57,8 +57,7 @@ private static SqliteValueGenerationStrategy GetValueGenerationStrategy(IConvent
var primaryKey = entityType.FindPrimaryKey();
if (primaryKey is { Properties.Count: 1 }
&& primaryKey.Properties[0] == property
- && property.ClrType.UnwrapNullableType().IsInteger()
- && property.FindTypeMapping()?.Converter == null)
+ && property.ClrType.UnwrapNullableType().IsInteger())
{
return SqliteValueGenerationStrategy.Autoincrement;
}
diff --git a/test/EFCore.Sqlite.FunctionalTests/SqliteAutoincrementWithConverterTest.cs b/test/EFCore.Sqlite.FunctionalTests/SqliteAutoincrementWithConverterTest.cs
new file mode 100644
index 00000000000..79acf3e2fe3
--- /dev/null
+++ b/test/EFCore.Sqlite.FunctionalTests/SqliteAutoincrementWithConverterTest.cs
@@ -0,0 +1,143 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+namespace Microsoft.EntityFrameworkCore;
+
+///
+/// Test for SQLite AUTOINCREMENT with value converters, specifically for issues #30699 and #29519.
+///
+public class SqliteAutoincrementWithConverterTest : IClassFixture
+{
+ private const string DatabaseName = "AutoincrementWithConverter";
+
+ public SqliteAutoincrementWithConverterTest(SqliteAutoincrementWithConverterFixture fixture)
+ {
+ Fixture = fixture;
+ }
+
+ protected SqliteAutoincrementWithConverterFixture Fixture { get; }
+
+ [ConditionalFact]
+ public virtual async Task Strongly_typed_id_with_converter_gets_autoincrement()
+ {
+ await using var context = (PoolableDbContext)CreateContext();
+
+ // Ensure the database is created
+ await context.Database.EnsureCreatedAsync();
+
+ // Check that the SQL contains AUTOINCREMENT for the strongly-typed ID
+ var sql = context.Database.GenerateCreateScript();
+ Assert.Contains("\"Id\" INTEGER NOT NULL CONSTRAINT \"PK_Products\" PRIMARY KEY AUTOINCREMENT", sql);
+ }
+
+ [ConditionalFact]
+ public virtual async Task Insert_with_strongly_typed_id_generates_value()
+ {
+ await using var context = (PoolableDbContext)CreateContext();
+ await context.Database.EnsureCreatedAsync();
+
+ // Insert a product with strongly-typed ID
+ var product = new Product { Name = "Test Product" };
+ context.Products.Add(product);
+ await context.SaveChangesAsync();
+
+ // The ID should have been generated
+ Assert.True(product.Id.Value > 0);
+
+ // Insert another product
+ var product2 = new Product { Name = "Test Product 2" };
+ context.Products.Add(product2);
+ await context.SaveChangesAsync();
+
+ // The second ID should be different
+ Assert.True(product2.Id.Value > product.Id.Value);
+ }
+
+ [ConditionalFact]
+ public virtual async Task Migration_consistency_with_value_converter()
+ {
+ await using var context = (PoolableDbContext)CreateContext();
+
+ // This test ensures that migrations don't generate repeated AlterColumn operations
+ // by checking that the model annotation is consistent
+ var property = context.Model.FindEntityType(typeof(Product))!.FindProperty(nameof(Product.Id))!;
+ var strategy = property.GetValueGenerationStrategy();
+
+ Assert.Equal(SqliteValueGenerationStrategy.Autoincrement, strategy);
+ }
+
+ [ConditionalFact]
+ public virtual async Task Explicit_autoincrement_configuration_is_honored()
+ {
+ await using var context = (PoolableDbContext)CreateContext();
+
+ // Check that explicitly configured AUTOINCREMENT is honored despite having a converter
+ var property = context.Model.FindEntityType(typeof(Product))!.FindProperty(nameof(Product.Id))!;
+ var strategy = property.GetValueGenerationStrategy();
+
+ Assert.Equal(SqliteValueGenerationStrategy.Autoincrement, strategy);
+
+ // Verify in the actual SQL generation
+ var sql = context.Database.GenerateCreateScript();
+ Assert.Contains("AUTOINCREMENT", sql);
+ }
+
+ protected virtual DbContext CreateContext()
+ => Fixture.CreateContext();
+
+ public class SqliteAutoincrementWithConverterFixture : SharedStoreFixtureBase
+ {
+ protected override string StoreName
+ => DatabaseName;
+
+ protected override ITestStoreFactory TestStoreFactory
+ => SqliteTestStoreFactory.Instance;
+
+ protected override IServiceCollection AddServices(IServiceCollection serviceCollection)
+ => base.AddServices(serviceCollection);
+
+ public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder)
+ => base.AddOptions(builder);
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context)
+ {
+ modelBuilder.Entity(b =>
+ {
+ b.Property(e => e.Id).HasConversion(
+ v => v.Value,
+ v => new ProductId(v));
+ b.Property(e => e.Id).UseAutoincrement(); // Explicit configuration
+ });
+
+ modelBuilder.Entity(); // Standard int ID for comparison
+ }
+ }
+
+ // Test entities
+ public record struct ProductId(int Value);
+
+ public class Product
+ {
+ public ProductId Id { get; set; }
+ public required string Name { get; set; }
+ }
+
+ public class Category
+ {
+ public int Id { get; set; }
+ public required string Name { get; set; }
+ }
+
+ public class PoolableDbContext : DbContext
+ {
+ public PoolableDbContext(DbContextOptions options)
+ : base(options)
+ {
+ }
+
+ public DbSet Products => Set();
+ public DbSet Categories => Set();
+ }
+}
\ No newline at end of file
From 21c588b829eb6ea47d839aac28c0491a04ce6cf0 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 5 Sep 2025 22:58:39 +0000
Subject: [PATCH 06/39] Add SqliteStoreGenerationConvention and
SqliteAnnotationCodeGenerator for first-class AUTOINCREMENT support
Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
---
.../Internal/SqliteAnnotationCodeGenerator.cs | 91 +++++++++++++++
.../Internal/SqliteDesignTimeServices.cs | 1 +
...iteComplexTypePropertyBuilderExtensions.cs | 31 +++++
.../Extensions/SqlitePropertyExtensions.cs | 15 ++-
.../SqliteServiceCollectionExtensions.cs | 1 -
.../Conventions/SqliteConventionSetBuilder.cs | 1 +
.../SqliteStoreGenerationConvention.cs | 106 ++++++++++++++++++
.../SqliteValueGenerationConvention.cs | 25 ++++-
.../SqliteMigrationsAnnotationProvider.cs | 46 --------
9 files changed, 265 insertions(+), 52 deletions(-)
create mode 100644 src/EFCore.Sqlite.Core/Design/Internal/SqliteAnnotationCodeGenerator.cs
create mode 100644 src/EFCore.Sqlite.Core/Metadata/Conventions/SqliteStoreGenerationConvention.cs
delete mode 100644 src/EFCore.Sqlite.Core/Migrations/Internal/SqliteMigrationsAnnotationProvider.cs
diff --git a/src/EFCore.Sqlite.Core/Design/Internal/SqliteAnnotationCodeGenerator.cs b/src/EFCore.Sqlite.Core/Design/Internal/SqliteAnnotationCodeGenerator.cs
new file mode 100644
index 00000000000..13114613720
--- /dev/null
+++ b/src/EFCore.Sqlite.Core/Design/Internal/SqliteAnnotationCodeGenerator.cs
@@ -0,0 +1,91 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.EntityFrameworkCore.Sqlite.Metadata.Internal;
+
+namespace Microsoft.EntityFrameworkCore.Sqlite.Design.Internal;
+
+///
+/// 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.
+///
+public class SqliteAnnotationCodeGenerator : AnnotationCodeGenerator
+{
+ #region MethodInfos
+
+ private static readonly MethodInfo PropertyUseAutoincrementMethodInfo
+ = typeof(SqlitePropertyBuilderExtensions).GetRuntimeMethod(
+ nameof(SqlitePropertyBuilderExtensions.UseAutoincrement), [typeof(PropertyBuilder)])!;
+
+ private static readonly MethodInfo ComplexTypePropertyUseAutoincrementMethodInfo
+ = typeof(SqliteComplexTypePropertyBuilderExtensions).GetRuntimeMethod(
+ nameof(SqliteComplexTypePropertyBuilderExtensions.UseAutoincrement), [typeof(ComplexTypePropertyBuilder)])!;
+
+ #endregion MethodInfos
+
+ ///
+ /// 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.
+ ///
+ public SqliteAnnotationCodeGenerator(AnnotationCodeGeneratorDependencies dependencies)
+ : base(dependencies)
+ {
+ }
+
+ ///
+ /// 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.
+ ///
+ public override IReadOnlyList GenerateFluentApiCalls(
+ IProperty property,
+ IDictionary annotations)
+ {
+ var fragments = new List(base.GenerateFluentApiCalls(property, annotations));
+
+ if (GetAndRemove(annotations, SqliteAnnotationNames.ValueGenerationStrategy) is { } strategy
+ && strategy == SqliteValueGenerationStrategy.Autoincrement)
+ {
+ var methodInfo = property.DeclaringType is IComplexType
+ ? ComplexTypePropertyUseAutoincrementMethodInfo
+ : PropertyUseAutoincrementMethodInfo;
+ fragments.Add(new MethodCallCodeFragment(methodInfo));
+ }
+
+ return fragments;
+ }
+
+ ///
+ /// 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.
+ ///
+ protected override bool IsHandledByConvention(IProperty property, IAnnotation annotation)
+ {
+ if (annotation.Name == SqliteAnnotationNames.ValueGenerationStrategy)
+ {
+ // Autoincrement strategy is handled by convention for single-column integer primary keys
+ return (SqliteValueGenerationStrategy)annotation.Value! == SqliteValueGenerationStrategy.None;
+ }
+
+ return base.IsHandledByConvention(property, annotation);
+ }
+
+ private static T? GetAndRemove(IDictionary annotations, string annotationName)
+ {
+ if (annotations.TryGetValue(annotationName, out var annotation)
+ && annotation.Value != null)
+ {
+ annotations.Remove(annotationName);
+ return (T)annotation.Value;
+ }
+
+ return default;
+ }
+}
\ No newline at end of file
diff --git a/src/EFCore.Sqlite.Core/Design/Internal/SqliteDesignTimeServices.cs b/src/EFCore.Sqlite.Core/Design/Internal/SqliteDesignTimeServices.cs
index 0ed5c42dfb4..9b40432265b 100644
--- a/src/EFCore.Sqlite.Core/Design/Internal/SqliteDesignTimeServices.cs
+++ b/src/EFCore.Sqlite.Core/Design/Internal/SqliteDesignTimeServices.cs
@@ -27,6 +27,7 @@ public virtual void ConfigureDesignTimeServices(IServiceCollection serviceCollec
serviceCollection.AddEntityFrameworkSqlite();
#pragma warning disable EF1001 // Internal EF Core API usage.
new EntityFrameworkRelationalDesignServicesBuilder(serviceCollection)
+ .TryAdd()
.TryAdd()
#pragma warning restore EF1001 // Internal EF Core API usage.
.TryAdd()
diff --git a/src/EFCore.Sqlite.Core/Extensions/SqliteComplexTypePropertyBuilderExtensions.cs b/src/EFCore.Sqlite.Core/Extensions/SqliteComplexTypePropertyBuilderExtensions.cs
index 17154e792eb..bd2e28ce5f5 100644
--- a/src/EFCore.Sqlite.Core/Extensions/SqliteComplexTypePropertyBuilderExtensions.cs
+++ b/src/EFCore.Sqlite.Core/Extensions/SqliteComplexTypePropertyBuilderExtensions.cs
@@ -12,6 +12,37 @@ namespace Microsoft.EntityFrameworkCore;
///
public static class SqliteComplexTypePropertyBuilderExtensions
{
+ ///
+ /// Configures the property to use the SQLite AUTOINCREMENT feature to generate values for new entities,
+ /// when targeting SQLite. This method sets the property's value generation strategy to .
+ ///
+ ///
+ /// See Modeling entity types and relationships, and
+ /// Accessing SQLite databases with EF Core for more information and examples.
+ ///
+ /// The builder for the property being configured.
+ /// The same builder instance so that multiple calls can be chained.
+ public static ComplexTypePropertyBuilder UseAutoincrement(this ComplexTypePropertyBuilder propertyBuilder)
+ {
+ propertyBuilder.Metadata.SetValueGenerationStrategy(SqliteValueGenerationStrategy.Autoincrement);
+
+ return propertyBuilder;
+ }
+
+ ///
+ /// Configures the property to use the SQLite AUTOINCREMENT feature to generate values for new entities,
+ /// when targeting SQLite. This method sets the property's value generation strategy to .
+ ///
+ ///
+ /// See Modeling entity types and relationships, and
+ /// Accessing SQLite databases with EF Core for more information and examples.
+ ///
+ /// The builder for the property being configured.
+ /// The same builder instance so that multiple calls can be chained.
+ public static ComplexTypePropertyBuilder UseAutoincrement(
+ this ComplexTypePropertyBuilder propertyBuilder)
+ => (ComplexTypePropertyBuilder)UseAutoincrement((ComplexTypePropertyBuilder)propertyBuilder);
+
///
/// Configures the SRID of the column that the property maps to when targeting SQLite.
///
diff --git a/src/EFCore.Sqlite.Core/Extensions/SqlitePropertyExtensions.cs b/src/EFCore.Sqlite.Core/Extensions/SqlitePropertyExtensions.cs
index 15bfe9b749c..095b75ff5d3 100644
--- a/src/EFCore.Sqlite.Core/Extensions/SqlitePropertyExtensions.cs
+++ b/src/EFCore.Sqlite.Core/Extensions/SqlitePropertyExtensions.cs
@@ -55,10 +55,23 @@ public static SqliteValueGenerationStrategy GetValueGenerationStrategy(
private static SqliteValueGenerationStrategy GetDefaultValueGenerationStrategy(IReadOnlyProperty property)
{
+ // Return None if default value, default value sql, or computed value are set
+ if (property.TryGetDefaultValue(out _)
+ || property.GetDefaultValueSql() != null
+ || property.GetComputedColumnSql() != null)
+ {
+ return SqliteValueGenerationStrategy.None;
+ }
+
+ // Return None if the property is part of a foreign key
+ if (property.IsForeignKey())
+ {
+ return SqliteValueGenerationStrategy.None;
+ }
+
var primaryKey = property.DeclaringType.ContainingEntityType.FindPrimaryKey();
return primaryKey is { Properties.Count: 1 }
&& primaryKey.Properties[0] == property
- && property.ValueGenerated == ValueGenerated.OnAdd
&& property.ClrType.UnwrapNullableType().IsInteger()
? SqliteValueGenerationStrategy.Autoincrement
: SqliteValueGenerationStrategy.None;
diff --git a/src/EFCore.Sqlite.Core/Extensions/SqliteServiceCollectionExtensions.cs b/src/EFCore.Sqlite.Core/Extensions/SqliteServiceCollectionExtensions.cs
index 50bcb6e07b3..c4c6f1680ce 100644
--- a/src/EFCore.Sqlite.Core/Extensions/SqliteServiceCollectionExtensions.cs
+++ b/src/EFCore.Sqlite.Core/Extensions/SqliteServiceCollectionExtensions.cs
@@ -95,7 +95,6 @@ public static IServiceCollection AddEntityFrameworkSqlite(this IServiceCollectio
.TryAdd()
.TryAdd()
.TryAdd()
- .TryAdd()
.TryAdd()
.TryAdd()
.TryAdd()
diff --git a/src/EFCore.Sqlite.Core/Metadata/Conventions/SqliteConventionSetBuilder.cs b/src/EFCore.Sqlite.Core/Metadata/Conventions/SqliteConventionSetBuilder.cs
index b37b2c6a407..d84a94fbeb4 100644
--- a/src/EFCore.Sqlite.Core/Metadata/Conventions/SqliteConventionSetBuilder.cs
+++ b/src/EFCore.Sqlite.Core/Metadata/Conventions/SqliteConventionSetBuilder.cs
@@ -46,6 +46,7 @@ public override ConventionSet CreateConventionSet()
conventionSet.Replace(new SqliteSharedTableConvention(Dependencies, RelationalDependencies));
conventionSet.Replace(new SqliteRuntimeModelConvention(Dependencies, RelationalDependencies));
conventionSet.Replace(new SqliteValueGenerationConvention(Dependencies, RelationalDependencies));
+ conventionSet.Replace(new SqliteStoreGenerationConvention(Dependencies, RelationalDependencies));
return conventionSet;
}
diff --git a/src/EFCore.Sqlite.Core/Metadata/Conventions/SqliteStoreGenerationConvention.cs b/src/EFCore.Sqlite.Core/Metadata/Conventions/SqliteStoreGenerationConvention.cs
new file mode 100644
index 00000000000..dc7a5e6d1ff
--- /dev/null
+++ b/src/EFCore.Sqlite.Core/Metadata/Conventions/SqliteStoreGenerationConvention.cs
@@ -0,0 +1,106 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.EntityFrameworkCore.Sqlite.Metadata.Internal;
+
+// ReSharper disable once CheckNamespace
+namespace Microsoft.EntityFrameworkCore.Metadata.Conventions;
+
+///
+/// A convention that ensures that properties aren't configured to have a default value, as computed column
+/// or using a at the same time.
+///
+///
+/// See Model building conventions, and
+/// Accessing SQLite databases with EF Core
+/// for more information and examples.
+///
+public class SqliteStoreGenerationConvention : StoreGenerationConvention
+{
+ ///
+ /// Creates a new instance of .
+ ///
+ /// Parameter object containing dependencies for this convention.
+ /// Parameter object containing relational dependencies for this convention.
+ public SqliteStoreGenerationConvention(
+ ProviderConventionSetBuilderDependencies dependencies,
+ RelationalConventionSetBuilderDependencies relationalDependencies)
+ : base(dependencies, relationalDependencies)
+ {
+ }
+
+ ///
+ /// Called after an annotation is changed on a property.
+ ///
+ /// The builder for the property.
+ /// The annotation name.
+ /// The new annotation.
+ /// The old annotation.
+ /// Additional information associated with convention execution.
+ public override void ProcessPropertyAnnotationChanged(
+ IConventionPropertyBuilder propertyBuilder,
+ string name,
+ IConventionAnnotation? annotation,
+ IConventionAnnotation? oldAnnotation,
+ IConventionContext context)
+ {
+ if (annotation == null
+ || oldAnnotation?.Value != null)
+ {
+ return;
+ }
+
+ var configurationSource = annotation.GetConfigurationSource();
+ var fromDataAnnotation = configurationSource != ConfigurationSource.Convention;
+ switch (name)
+ {
+ case RelationalAnnotationNames.DefaultValue:
+ if (propertyBuilder.HasValueGenerationStrategy(null, fromDataAnnotation) == null
+ && propertyBuilder.HasDefaultValue(null, fromDataAnnotation) != null)
+ {
+ context.StopProcessing();
+ return;
+ }
+
+ break;
+ case RelationalAnnotationNames.DefaultValueSql:
+ if (propertyBuilder.HasValueGenerationStrategy(null, fromDataAnnotation) == null
+ && propertyBuilder.HasDefaultValueSql(null, fromDataAnnotation) != null)
+ {
+ context.StopProcessing();
+ return;
+ }
+
+ break;
+ case RelationalAnnotationNames.ComputedColumnSql:
+ if (propertyBuilder.HasValueGenerationStrategy(null, fromDataAnnotation) == null
+ && propertyBuilder.HasComputedColumnSql(null, fromDataAnnotation) != null)
+ {
+ context.StopProcessing();
+ return;
+ }
+
+ break;
+ case SqliteAnnotationNames.ValueGenerationStrategy:
+ if ((propertyBuilder.HasDefaultValue(null, fromDataAnnotation) == null
+ || propertyBuilder.HasDefaultValueSql(null, fromDataAnnotation) == null
+ || propertyBuilder.HasComputedColumnSql(null, fromDataAnnotation) == null)
+ && propertyBuilder.HasValueGenerationStrategy(null, fromDataAnnotation) != null)
+ {
+ context.StopProcessing();
+ return;
+ }
+
+ break;
+ }
+
+ base.ProcessPropertyAnnotationChanged(propertyBuilder, name, annotation, oldAnnotation, context);
+ }
+
+ ///
+ protected override void Validate(IConventionProperty property, in StoreObjectIdentifier storeObject)
+ {
+ // Simple validation without detailed warnings for now
+ base.Validate(property, storeObject);
+ }
+}
\ No newline at end of file
diff --git a/src/EFCore.Sqlite.Core/Metadata/Conventions/SqliteValueGenerationConvention.cs b/src/EFCore.Sqlite.Core/Metadata/Conventions/SqliteValueGenerationConvention.cs
index a2c05c26aa1..ec81d7670b7 100644
--- a/src/EFCore.Sqlite.Core/Metadata/Conventions/SqliteValueGenerationConvention.cs
+++ b/src/EFCore.Sqlite.Core/Metadata/Conventions/SqliteValueGenerationConvention.cs
@@ -40,9 +40,7 @@ public SqliteValueGenerationConvention(
/// The strategy to set for the property.
protected override ValueGenerated? GetValueGenerated(IConventionProperty property)
{
- var declaringType = property.DeclaringType;
-
- var strategy = GetValueGenerationStrategy(property);
+ var strategy = property.GetValueGenerationStrategy();
if (strategy == SqliteValueGenerationStrategy.Autoincrement)
{
return ValueGenerated.OnAdd;
@@ -51,8 +49,27 @@ public SqliteValueGenerationConvention(
return base.GetValueGenerated(property);
}
- private static SqliteValueGenerationStrategy GetValueGenerationStrategy(IConventionProperty property)
+ ///
+ /// Returns the default value generation strategy for the property.
+ ///
+ /// The property.
+ /// The default strategy for the property.
+ private static SqliteValueGenerationStrategy GetDefaultValueGenerationStrategy(IConventionProperty property)
{
+ // Return None if default value, default value sql, or computed value are set
+ if (property.TryGetDefaultValue(out _)
+ || property.GetDefaultValueSql() != null
+ || property.GetComputedColumnSql() != null)
+ {
+ return SqliteValueGenerationStrategy.None;
+ }
+
+ // Return None if the property is part of a foreign key
+ if (property.IsForeignKey())
+ {
+ return SqliteValueGenerationStrategy.None;
+ }
+
var entityType = (IConventionEntityType)property.DeclaringType;
var primaryKey = entityType.FindPrimaryKey();
if (primaryKey is { Properties.Count: 1 }
diff --git a/src/EFCore.Sqlite.Core/Migrations/Internal/SqliteMigrationsAnnotationProvider.cs b/src/EFCore.Sqlite.Core/Migrations/Internal/SqliteMigrationsAnnotationProvider.cs
deleted file mode 100644
index e5ba15c99cc..00000000000
--- a/src/EFCore.Sqlite.Core/Migrations/Internal/SqliteMigrationsAnnotationProvider.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using Microsoft.EntityFrameworkCore.Sqlite.Metadata.Internal;
-
-namespace Microsoft.EntityFrameworkCore.Sqlite.Migrations.Internal;
-
-///
-/// 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.
-///
-public class SqliteMigrationsAnnotationProvider : MigrationsAnnotationProvider
-{
- ///
- /// Initializes a new instance of this class.
- ///
- /// Parameter object containing dependencies for this service.
-#pragma warning disable EF1001 // Internal EF Core API usage.
- public SqliteMigrationsAnnotationProvider(MigrationsAnnotationProviderDependencies dependencies)
-#pragma warning restore EF1001 // Internal EF Core API usage.
- : base(dependencies)
- {
- }
-
- ///
- public override IEnumerable ForRemove(IColumn column)
- {
- // Preserve the autoincrement annotation when removing columns for SQLite migrations
- if (column[SqliteAnnotationNames.Autoincrement] as bool? == true)
- {
- yield return new Annotation(SqliteAnnotationNames.Autoincrement, true);
- }
- }
-
- ///
- public override IEnumerable ForRename(IColumn column)
- {
- // Preserve the autoincrement annotation when renaming columns for SQLite migrations
- if (column[SqliteAnnotationNames.Autoincrement] as bool? == true)
- {
- yield return new Annotation(SqliteAnnotationNames.Autoincrement, true);
- }
- }
-}
\ No newline at end of file
From 2698227615a8c660fc95ebf472433c5f1ddc65d9 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 5 Sep 2025 23:13:09 +0000
Subject: [PATCH 07/39] Address review feedback: move tests, fix value
generation convention logic, and add comprehensive test coverage
Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
---
.../Migrations/MigrationsSqliteTest.cs | 214 ++++++++++++++++++
.../Scaffolding/CompiledModelSqliteTest.cs | 23 ++
.../SqliteAutoincrementWithConverterTest.cs | 143 ------------
.../SqliteMetadataExtensionsTest.cs | 214 ++++++++++++++++++
.../SqliteValueGenerationStrategyTest.cs | 138 -----------
.../Migrations/SqliteModelDifferTest.cs | 137 +++++++++++
6 files changed, 588 insertions(+), 281 deletions(-)
delete mode 100644 test/EFCore.Sqlite.FunctionalTests/SqliteAutoincrementWithConverterTest.cs
delete mode 100644 test/EFCore.Sqlite.Tests/Extensions/SqliteValueGenerationStrategyTest.cs
create mode 100644 test/EFCore.Sqlite.Tests/Migrations/SqliteModelDifferTest.cs
diff --git a/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsSqliteTest.cs
index 43cc3985995..b68911714a8 100644
--- a/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsSqliteTest.cs
+++ b/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsSqliteTest.cs
@@ -2348,6 +2348,220 @@ await Test(
""");
}
+ [ConditionalFact]
+ public virtual async Task Create_table_with_autoincrement_column()
+ {
+ await Test(
+ builder => { },
+ builder => builder.Entity(
+ "Product",
+ x =>
+ {
+ x.Property("Id").UseAutoincrement();
+ x.HasKey("Id");
+ x.Property("Name");
+ }),
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ Assert.Equal("Product", table.Name);
+ Assert.Equal(2, table.Columns.Count());
+
+ var idColumn = Assert.Single(table.Columns, c => c.Name == "Id");
+ Assert.False(idColumn.IsNullable);
+ });
+
+ AssertSql(
+ """
+CREATE TABLE "Product" (
+ "Id" INTEGER NOT NULL CONSTRAINT "PK_Product" PRIMARY KEY AUTOINCREMENT,
+ "Name" TEXT NULL
+);
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Create_table_with_autoincrement_and_value_converter()
+ {
+ await Test(
+ builder => { },
+ builder => builder.Entity(
+ x =>
+ {
+ x.Property(e => e.Id).HasConversion(
+ v => v.Value,
+ v => new ProductId(v)).UseAutoincrement();
+ x.HasKey(e => e.Id);
+ x.Property(e => e.Name);
+ }),
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ Assert.Equal("ProductWithStrongId", table.Name);
+ Assert.Equal(2, table.Columns.Count());
+
+ var idColumn = Assert.Single(table.Columns, c => c.Name == "Id");
+ Assert.False(idColumn.IsNullable);
+ });
+
+ AssertSql(
+ """
+CREATE TABLE "ProductWithStrongId" (
+ "Id" INTEGER NOT NULL CONSTRAINT "PK_ProductWithStrongId" PRIMARY KEY AUTOINCREMENT,
+ "Name" TEXT NULL
+);
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Create_table_with_composite_primary_key_and_autoincrement_fails()
+ {
+ await AssertNotSupportedAsync(
+ () => Test(
+ builder => { },
+ builder => builder.Entity(
+ "CompositeEntity",
+ x =>
+ {
+ x.Property("Id1").UseAutoincrement();
+ x.Property("Id2");
+ x.HasKey("Id1", "Id2");
+ }),
+ model => { }),
+ "SQLite AUTOINCREMENT can only be used with a single primary key column.");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Alter_column_add_autoincrement()
+ {
+ await Test(
+ builder => builder.Entity(
+ "Product",
+ x =>
+ {
+ x.Property("Id");
+ x.HasKey("Id");
+ x.Property("Name");
+ }),
+ builder => builder.Entity(
+ "Product",
+ x =>
+ {
+ x.Property("Id").UseAutoincrement();
+ x.HasKey("Id");
+ x.Property("Name");
+ }),
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ Assert.Equal("Product", table.Name);
+ Assert.Equal(2, table.Columns.Count());
+
+ var idColumn = Assert.Single(table.Columns, c => c.Name == "Id");
+ Assert.False(idColumn.IsNullable);
+ });
+
+ AssertSql(
+ """
+CREATE TABLE "ef_temp_Product" (
+ "Id" INTEGER NOT NULL CONSTRAINT "PK_Product" PRIMARY KEY AUTOINCREMENT,
+ "Name" TEXT NULL
+);
+""",
+ //
+ """
+INSERT INTO "ef_temp_Product" ("Name")
+SELECT "Name"
+FROM "Product";
+""",
+ //
+ """
+PRAGMA foreign_keys = 0;
+""",
+ //
+ """
+DROP TABLE "Product";
+""",
+ //
+ """
+ALTER TABLE "ef_temp_Product" RENAME TO "Product";
+""",
+ //
+ """
+PRAGMA foreign_keys = 1;
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Alter_column_remove_autoincrement()
+ {
+ await Test(
+ builder => builder.Entity(
+ "Product",
+ x =>
+ {
+ x.Property("Id").UseAutoincrement();
+ x.HasKey("Id");
+ x.Property("Name");
+ }),
+ builder => builder.Entity(
+ "Product",
+ x =>
+ {
+ x.Property("Id");
+ x.HasKey("Id");
+ x.Property("Name");
+ }),
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ Assert.Equal("Product", table.Name);
+ Assert.Equal(2, table.Columns.Count());
+
+ var idColumn = Assert.Single(table.Columns, c => c.Name == "Id");
+ Assert.False(idColumn.IsNullable);
+ });
+
+ AssertSql(
+ """
+CREATE TABLE "ef_temp_Product" (
+ "Id" INTEGER NOT NULL CONSTRAINT "PK_Product" PRIMARY KEY,
+ "Name" TEXT NULL
+);
+""",
+ //
+ """
+INSERT INTO "ef_temp_Product" ("Id", "Name")
+SELECT "Id", "Name"
+FROM "Product";
+""",
+ //
+ """
+PRAGMA foreign_keys = 0;
+""",
+ //
+ """
+DROP TABLE "Product";
+""",
+ //
+ """
+ALTER TABLE "ef_temp_Product" RENAME TO "Product";
+""",
+ //
+ """
+PRAGMA foreign_keys = 1;
+""");
+ }
+
+ // Test entities for autoincrement tests
+ public record struct ProductId(int Value);
+
+ public class ProductWithStrongId
+ {
+ public ProductId Id { get; set; }
+ public string? Name { get; set; }
+ }
+
protected virtual async Task AssertNotSupportedAsync(Func action, string? message = null)
{
var ex = await Assert.ThrowsAsync(action);
diff --git a/test/EFCore.Sqlite.FunctionalTests/Scaffolding/CompiledModelSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Scaffolding/CompiledModelSqliteTest.cs
index fe8f4a4772f..9a0a4b34681 100644
--- a/test/EFCore.Sqlite.FunctionalTests/Scaffolding/CompiledModelSqliteTest.cs
+++ b/test/EFCore.Sqlite.FunctionalTests/Scaffolding/CompiledModelSqliteTest.cs
@@ -27,6 +27,14 @@ protected override void BuildBigModel(ModelBuilder modelBuilder, bool jsonColumn
.HasSrid(1101);
});
+ modelBuilder.Entity(eb =>
+ {
+ eb.Property("Id");
+ eb.HasKey("Id");
+ // This should be auto-configured by convention to use AUTOINCREMENT
+ eb.Property("Name");
+ });
+
modelBuilder.Entity(eb =>
{
eb.Property("Point")
@@ -76,6 +84,15 @@ protected override void AssertBigModel(IModel model, bool jsonColumns)
Assert.IsType>(pointProperty.GetKeyValueComparer());
Assert.IsType>(pointProperty.GetProviderValueComparer());
Assert.Null(pointProperty[CoreAnnotationNames.PropertyAccessMode]);
+
+ var autoIncrementEntity = model.FindEntityType(typeof(AutoIncrementEntity))!;
+ var idProperty = autoIncrementEntity.FindProperty("Id")!;
+ Assert.Equal(typeof(int), idProperty.ClrType);
+ Assert.False(idProperty.IsNullable);
+ Assert.Equal(ValueGenerated.OnAdd, idProperty.ValueGenerated);
+ Assert.Equal("Id", idProperty.GetColumnName());
+ Assert.Equal("INTEGER", idProperty.GetColumnType());
+ Assert.Equal(SqliteValueGenerationStrategy.Autoincrement, idProperty.GetValueGenerationStrategy());
}
//Sprocs not supported
@@ -117,4 +134,10 @@ protected override BuildSource AddReferences(BuildSource build, [CallerFilePath]
build.References.Add(BuildReference.ByName("NetTopologySuite"));
return build;
}
+
+ public class AutoIncrementEntity
+ {
+ public int Id { get; set; }
+ public string? Name { get; set; }
+ }
}
diff --git a/test/EFCore.Sqlite.FunctionalTests/SqliteAutoincrementWithConverterTest.cs b/test/EFCore.Sqlite.FunctionalTests/SqliteAutoincrementWithConverterTest.cs
deleted file mode 100644
index 79acf3e2fe3..00000000000
--- a/test/EFCore.Sqlite.FunctionalTests/SqliteAutoincrementWithConverterTest.cs
+++ /dev/null
@@ -1,143 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-
-namespace Microsoft.EntityFrameworkCore;
-
-///
-/// Test for SQLite AUTOINCREMENT with value converters, specifically for issues #30699 and #29519.
-///
-public class SqliteAutoincrementWithConverterTest : IClassFixture
-{
- private const string DatabaseName = "AutoincrementWithConverter";
-
- public SqliteAutoincrementWithConverterTest(SqliteAutoincrementWithConverterFixture fixture)
- {
- Fixture = fixture;
- }
-
- protected SqliteAutoincrementWithConverterFixture Fixture { get; }
-
- [ConditionalFact]
- public virtual async Task Strongly_typed_id_with_converter_gets_autoincrement()
- {
- await using var context = (PoolableDbContext)CreateContext();
-
- // Ensure the database is created
- await context.Database.EnsureCreatedAsync();
-
- // Check that the SQL contains AUTOINCREMENT for the strongly-typed ID
- var sql = context.Database.GenerateCreateScript();
- Assert.Contains("\"Id\" INTEGER NOT NULL CONSTRAINT \"PK_Products\" PRIMARY KEY AUTOINCREMENT", sql);
- }
-
- [ConditionalFact]
- public virtual async Task Insert_with_strongly_typed_id_generates_value()
- {
- await using var context = (PoolableDbContext)CreateContext();
- await context.Database.EnsureCreatedAsync();
-
- // Insert a product with strongly-typed ID
- var product = new Product { Name = "Test Product" };
- context.Products.Add(product);
- await context.SaveChangesAsync();
-
- // The ID should have been generated
- Assert.True(product.Id.Value > 0);
-
- // Insert another product
- var product2 = new Product { Name = "Test Product 2" };
- context.Products.Add(product2);
- await context.SaveChangesAsync();
-
- // The second ID should be different
- Assert.True(product2.Id.Value > product.Id.Value);
- }
-
- [ConditionalFact]
- public virtual async Task Migration_consistency_with_value_converter()
- {
- await using var context = (PoolableDbContext)CreateContext();
-
- // This test ensures that migrations don't generate repeated AlterColumn operations
- // by checking that the model annotation is consistent
- var property = context.Model.FindEntityType(typeof(Product))!.FindProperty(nameof(Product.Id))!;
- var strategy = property.GetValueGenerationStrategy();
-
- Assert.Equal(SqliteValueGenerationStrategy.Autoincrement, strategy);
- }
-
- [ConditionalFact]
- public virtual async Task Explicit_autoincrement_configuration_is_honored()
- {
- await using var context = (PoolableDbContext)CreateContext();
-
- // Check that explicitly configured AUTOINCREMENT is honored despite having a converter
- var property = context.Model.FindEntityType(typeof(Product))!.FindProperty(nameof(Product.Id))!;
- var strategy = property.GetValueGenerationStrategy();
-
- Assert.Equal(SqliteValueGenerationStrategy.Autoincrement, strategy);
-
- // Verify in the actual SQL generation
- var sql = context.Database.GenerateCreateScript();
- Assert.Contains("AUTOINCREMENT", sql);
- }
-
- protected virtual DbContext CreateContext()
- => Fixture.CreateContext();
-
- public class SqliteAutoincrementWithConverterFixture : SharedStoreFixtureBase
- {
- protected override string StoreName
- => DatabaseName;
-
- protected override ITestStoreFactory TestStoreFactory
- => SqliteTestStoreFactory.Instance;
-
- protected override IServiceCollection AddServices(IServiceCollection serviceCollection)
- => base.AddServices(serviceCollection);
-
- public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder)
- => base.AddOptions(builder);
-
- protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context)
- {
- modelBuilder.Entity(b =>
- {
- b.Property(e => e.Id).HasConversion(
- v => v.Value,
- v => new ProductId(v));
- b.Property(e => e.Id).UseAutoincrement(); // Explicit configuration
- });
-
- modelBuilder.Entity(); // Standard int ID for comparison
- }
- }
-
- // Test entities
- public record struct ProductId(int Value);
-
- public class Product
- {
- public ProductId Id { get; set; }
- public required string Name { get; set; }
- }
-
- public class Category
- {
- public int Id { get; set; }
- public required string Name { get; set; }
- }
-
- public class PoolableDbContext : DbContext
- {
- public PoolableDbContext(DbContextOptions options)
- : base(options)
- {
- }
-
- public DbSet Products => Set();
- public DbSet Categories => Set();
- }
-}
\ No newline at end of file
diff --git a/test/EFCore.Sqlite.Tests/Extensions/SqliteMetadataExtensionsTest.cs b/test/EFCore.Sqlite.Tests/Extensions/SqliteMetadataExtensionsTest.cs
index 3b664c18c79..a0e6080ded1 100644
--- a/test/EFCore.Sqlite.Tests/Extensions/SqliteMetadataExtensionsTest.cs
+++ b/test/EFCore.Sqlite.Tests/Extensions/SqliteMetadataExtensionsTest.cs
@@ -26,9 +26,223 @@ public void Can_get_and_set_srid()
Assert.Null(property.GetSrid());
}
+ [ConditionalFact]
+ public void Can_get_and_set_value_generation_strategy()
+ {
+ var modelBuilder = new ModelBuilder();
+
+ var property = modelBuilder
+ .Entity()
+ .Property(e => e.Id)
+ .Metadata;
+
+ Assert.Equal(SqliteValueGenerationStrategy.None, property.GetValueGenerationStrategy());
+
+ property.SetValueGenerationStrategy(SqliteValueGenerationStrategy.Autoincrement);
+
+ Assert.Equal(SqliteValueGenerationStrategy.Autoincrement, property.GetValueGenerationStrategy());
+
+ property.SetValueGenerationStrategy(null);
+
+ Assert.Equal(SqliteValueGenerationStrategy.None, property.GetValueGenerationStrategy());
+ }
+
+ [ConditionalFact]
+ public void Can_set_value_generation_strategy_on_mutable_property()
+ {
+ var modelBuilder = new ModelBuilder();
+
+ var property = (IMutableProperty)modelBuilder
+ .Entity()
+ .Property(e => e.Id)
+ .Metadata;
+
+ Assert.Equal(SqliteValueGenerationStrategy.None, property.GetValueGenerationStrategy());
+
+ ((IMutableProperty)property).SetValueGenerationStrategy(SqliteValueGenerationStrategy.Autoincrement);
+
+ Assert.Equal(SqliteValueGenerationStrategy.Autoincrement, property.GetValueGenerationStrategy());
+ }
+
+ [ConditionalFact]
+ public void UseAutoincrement_sets_value_generation_strategy()
+ {
+ var modelBuilder = new ModelBuilder();
+
+ var propertyBuilder = modelBuilder
+ .Entity()
+ .Property(e => e.Id);
+
+ propertyBuilder.UseAutoincrement();
+
+ Assert.Equal(SqliteValueGenerationStrategy.Autoincrement, propertyBuilder.Metadata.GetValueGenerationStrategy());
+ }
+
+ [ConditionalFact]
+ public void Generic_UseAutoincrement_sets_value_generation_strategy()
+ {
+ var modelBuilder = new ModelBuilder();
+
+ var propertyBuilder = modelBuilder
+ .Entity()
+ .Property(e => e.Id);
+
+ propertyBuilder.UseAutoincrement();
+
+ Assert.Equal(SqliteValueGenerationStrategy.Autoincrement, propertyBuilder.Metadata.GetValueGenerationStrategy());
+ }
+
+ [ConditionalFact]
+ public void Default_value_generation_strategy_for_integer_primary_key()
+ {
+ var modelBuilder = new ModelBuilder();
+
+ var property = modelBuilder
+ .Entity()
+ .Property(e => e.Id)
+ .Metadata;
+
+ // Without conventions, the default should be None
+ Assert.Equal(SqliteValueGenerationStrategy.None, property.GetValueGenerationStrategy());
+ }
+
+ [ConditionalFact]
+ public void No_autoincrement_for_non_primary_key()
+ {
+ var modelBuilder = new ModelBuilder();
+
+ var property = modelBuilder
+ .Entity()
+ .Property(e => e.OtherId)
+ .Metadata;
+
+ Assert.Equal(SqliteValueGenerationStrategy.None, property.GetValueGenerationStrategy());
+ }
+
+ [ConditionalFact]
+ public void No_autoincrement_for_non_integer_primary_key()
+ {
+ var modelBuilder = new ModelBuilder();
+
+ var property = modelBuilder
+ .Entity()
+ .Property(e => e.Id)
+ .Metadata;
+
+ Assert.Equal(SqliteValueGenerationStrategy.None, property.GetValueGenerationStrategy());
+ }
+
+ [ConditionalFact]
+ public void No_autoincrement_for_composite_primary_key()
+ {
+ var modelBuilder = new ModelBuilder();
+
+ modelBuilder
+ .Entity(b =>
+ {
+ b.HasKey(e => new { e.Id1, e.Id2 });
+ });
+
+ var property1 = modelBuilder.Entity().Property(e => e.Id1).Metadata;
+ var property2 = modelBuilder.Entity().Property(e => e.Id2).Metadata;
+
+ Assert.Equal(SqliteValueGenerationStrategy.None, property1.GetValueGenerationStrategy());
+ Assert.Equal(SqliteValueGenerationStrategy.None, property2.GetValueGenerationStrategy());
+ }
+
+ [ConditionalFact]
+ public void No_autoincrement_when_default_value_set()
+ {
+ var modelBuilder = new ModelBuilder();
+
+ var property = modelBuilder
+ .Entity()
+ .Property(e => e.Id)
+ .HasDefaultValue(42)
+ .Metadata;
+
+ Assert.Equal(SqliteValueGenerationStrategy.None, property.GetValueGenerationStrategy());
+ }
+
+ [ConditionalFact]
+ public void No_autoincrement_when_default_value_sql_set()
+ {
+ var modelBuilder = new ModelBuilder();
+
+ var property = modelBuilder
+ .Entity()
+ .Property(e => e.Id)
+ .HasDefaultValueSql("1")
+ .Metadata;
+
+ Assert.Equal(SqliteValueGenerationStrategy.None, property.GetValueGenerationStrategy());
+ }
+
+ [ConditionalFact]
+ public void No_autoincrement_when_computed_column_sql_set()
+ {
+ var modelBuilder = new ModelBuilder();
+
+ var property = modelBuilder
+ .Entity()
+ .Property(e => e.Id)
+ .HasComputedColumnSql("1")
+ .Metadata;
+
+ Assert.Equal(SqliteValueGenerationStrategy.None, property.GetValueGenerationStrategy());
+ }
+
+ [ConditionalFact]
+ public void No_autoincrement_when_property_is_foreign_key()
+ {
+ var modelBuilder = new ModelBuilder();
+
+ modelBuilder.Entity(b =>
+ {
+ b.HasKey(e => e.Id);
+ b.Property(e => e.CustomerId);
+ b.HasOne()
+ .WithMany()
+ .HasForeignKey(e => e.CustomerId);
+ });
+
+ var property = modelBuilder.Entity().Property(e => e.CustomerId).Metadata;
+
+ Assert.Equal(SqliteValueGenerationStrategy.None, property.GetValueGenerationStrategy());
+ }
+
private class Customer
{
public int Id { get; set; }
+ public int OtherId { get; set; }
+ public string? Name { get; set; }
+ public string? Geometry { get; set; }
+ }
+
+ private class CustomerWithStringKey
+ {
+ public string Id { get; set; } = null!;
+ public string? Name { get; set; }
+ }
+
+ private class CustomerWithCompositeKey
+ {
+ public int Id1 { get; set; }
+ public int Id2 { get; set; }
+ public string? Name { get; set; }
+ }
+
+ private class Order
+ {
+ public int Id { get; set; }
+ public int CustomerId { get; set; }
+ }
+
+ private class CustomerNoPK
+ {
+ public int Id { get; set; }
+ public int OtherId { get; set; }
+ public string? Name { get; set; }
public string? Geometry { get; set; }
}
}
diff --git a/test/EFCore.Sqlite.Tests/Extensions/SqliteValueGenerationStrategyTest.cs b/test/EFCore.Sqlite.Tests/Extensions/SqliteValueGenerationStrategyTest.cs
deleted file mode 100644
index 82374952500..00000000000
--- a/test/EFCore.Sqlite.Tests/Extensions/SqliteValueGenerationStrategyTest.cs
+++ /dev/null
@@ -1,138 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace Microsoft.EntityFrameworkCore;
-
-public class SqliteValueGenerationStrategyTest
-{
- [ConditionalFact]
- public void Can_get_and_set_value_generation_strategy()
- {
- var modelBuilder = new ModelBuilder();
-
- var property = modelBuilder
- .Entity()
- .Property(e => e.Id)
- .Metadata;
-
- Assert.Equal(SqliteValueGenerationStrategy.None, property.GetValueGenerationStrategy());
-
- property.SetValueGenerationStrategy(SqliteValueGenerationStrategy.Autoincrement);
-
- Assert.Equal(SqliteValueGenerationStrategy.Autoincrement, property.GetValueGenerationStrategy());
-
- property.SetValueGenerationStrategy(null);
-
- Assert.Equal(SqliteValueGenerationStrategy.None, property.GetValueGenerationStrategy());
- }
-
- [ConditionalFact]
- public void UseAutoincrement_sets_value_generation_strategy()
- {
- var modelBuilder = new ModelBuilder();
-
- var propertyBuilder = modelBuilder
- .Entity()
- .Property(e => e.Id);
-
- propertyBuilder.UseAutoincrement();
-
- Assert.Equal(SqliteValueGenerationStrategy.Autoincrement, propertyBuilder.Metadata.GetValueGenerationStrategy());
- }
-
- [ConditionalFact]
- public void Generic_UseAutoincrement_sets_value_generation_strategy()
- {
- var modelBuilder = new ModelBuilder();
-
- var propertyBuilder = modelBuilder
- .Entity()
- .Property(e => e.Id);
-
- propertyBuilder.UseAutoincrement();
-
- Assert.Equal(SqliteValueGenerationStrategy.Autoincrement, propertyBuilder.Metadata.GetValueGenerationStrategy());
- }
-
- [ConditionalFact]
- public void Default_value_generation_strategy_for_integer_primary_key()
- {
- var modelBuilder = new ModelBuilder();
-
- var property = modelBuilder
- .Entity()
- .Property(e => e.Id)
- .Metadata;
-
- // Without conventions, the default should be None
- Assert.Equal(SqliteValueGenerationStrategy.None, property.GetValueGenerationStrategy());
- }
-
- [ConditionalFact]
- public void No_autoincrement_for_non_primary_key()
- {
- var modelBuilder = new ModelBuilder();
-
- var property = modelBuilder
- .Entity()
- .Property(e => e.OtherId)
- .Metadata;
-
- Assert.Equal(SqliteValueGenerationStrategy.None, property.GetValueGenerationStrategy());
- Assert.Equal(ValueGenerated.Never, property.ValueGenerated);
- }
-
- [ConditionalFact]
- public void No_autoincrement_for_non_integer_primary_key()
- {
- var modelBuilder = new ModelBuilder();
-
- var property = modelBuilder
- .Entity()
- .Property(e => e.Id)
- .Metadata;
-
- Assert.Equal(SqliteValueGenerationStrategy.None, property.GetValueGenerationStrategy());
- Assert.Equal(ValueGenerated.Never, property.ValueGenerated);
- }
-
- [ConditionalFact]
- public void No_autoincrement_for_composite_primary_key()
- {
- var modelBuilder = new ModelBuilder();
-
- modelBuilder
- .Entity(b =>
- {
- b.HasKey(e => new { e.Id1, e.Id2 });
- });
-
- var property1 = modelBuilder.Entity().Property(e => e.Id1).Metadata;
- var property2 = modelBuilder.Entity().Property(e => e.Id2).Metadata;
-
- Assert.Equal(SqliteValueGenerationStrategy.None, property1.GetValueGenerationStrategy());
- Assert.Equal(SqliteValueGenerationStrategy.None, property2.GetValueGenerationStrategy());
- Assert.Equal(ValueGenerated.Never, property1.ValueGenerated);
- Assert.Equal(ValueGenerated.Never, property2.ValueGenerated);
- }
-
- private class Customer
- {
- public int Id { get; set; }
- public int OtherId { get; set; }
- public string? Name { get; set; }
- }
-
- private class CustomerWithStringKey
- {
- public string Id { get; set; } = null!;
- public string? Name { get; set; }
- }
-
- private class CustomerWithCompositeKey
- {
- public int Id1 { get; set; }
- public int Id2 { get; set; }
- public string? Name { get; set; }
- }
-}
\ No newline at end of file
diff --git a/test/EFCore.Sqlite.Tests/Migrations/SqliteModelDifferTest.cs b/test/EFCore.Sqlite.Tests/Migrations/SqliteModelDifferTest.cs
new file mode 100644
index 00000000000..fd63e9eadef
--- /dev/null
+++ b/test/EFCore.Sqlite.Tests/Migrations/SqliteModelDifferTest.cs
@@ -0,0 +1,137 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.EntityFrameworkCore.Migrations.Internal;
+using Microsoft.EntityFrameworkCore.Sqlite.Metadata.Internal;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+// ReSharper disable InconsistentNaming
+namespace Microsoft.EntityFrameworkCore.Migrations;
+
+public class SqliteModelDifferTest : MigrationsModelDifferTestBase
+{
+ [ConditionalFact]
+ public void Add_property_with_autoincrement_strategy()
+ => Execute(
+ _ => { },
+ target => target.Entity(
+ "Person",
+ x =>
+ {
+ x.Property("Id");
+ x.HasKey("Id");
+ x.Property("Id").UseAutoincrement();
+ }),
+ upOps =>
+ {
+ Assert.Equal(2, upOps.Count);
+
+ var createTableOperation = Assert.IsType(upOps[0]);
+ var idColumn = createTableOperation.Columns.Single(c => c.Name == "Id");
+ Assert.Equal(SqliteValueGenerationStrategy.Autoincrement, idColumn[SqliteAnnotationNames.ValueGenerationStrategy]);
+
+ Assert.IsType(upOps[1]);
+ });
+
+ [ConditionalFact]
+ public void Alter_property_add_autoincrement_strategy()
+ => Execute(
+ common => common.Entity(
+ "Person",
+ x =>
+ {
+ x.Property("Id");
+ x.HasKey("Id");
+ }),
+ source => source.Entity("Person").Property("Id"),
+ target => target.Entity("Person").Property("Id").UseAutoincrement(),
+ upOps =>
+ {
+ Assert.Equal(1, upOps.Count);
+
+ var alterColumnOperation = Assert.IsType(upOps[0]);
+ Assert.Equal(SqliteValueGenerationStrategy.Autoincrement, alterColumnOperation[SqliteAnnotationNames.ValueGenerationStrategy]);
+ Assert.Null(alterColumnOperation.OldColumn[SqliteAnnotationNames.ValueGenerationStrategy]);
+ });
+
+ [ConditionalFact]
+ public void Alter_property_remove_autoincrement_strategy()
+ => Execute(
+ common => common.Entity(
+ "Person",
+ x =>
+ {
+ x.Property("Id");
+ x.HasKey("Id");
+ }),
+ source => source.Entity("Person").Property("Id").UseAutoincrement(),
+ target => target.Entity("Person").Property("Id"),
+ upOps =>
+ {
+ Assert.Equal(1, upOps.Count);
+
+ var alterColumnOperation = Assert.IsType(upOps[0]);
+ Assert.Null(alterColumnOperation[SqliteAnnotationNames.ValueGenerationStrategy]);
+ Assert.Equal(SqliteValueGenerationStrategy.Autoincrement, alterColumnOperation.OldColumn[SqliteAnnotationNames.ValueGenerationStrategy]);
+ });
+
+ [ConditionalFact]
+ public void Autoincrement_with_value_converter_generates_consistent_migrations()
+ => Execute(
+ common => common.Entity(
+ x =>
+ {
+ x.Property(e => e.Id).HasConversion(
+ v => v.Value,
+ v => new ProductId(v));
+ x.HasKey(e => e.Id);
+ }),
+ source => source.Entity().Property(e => e.Id),
+ target => target.Entity().Property(e => e.Id).UseAutoincrement(),
+ upOps =>
+ {
+ Assert.Equal(1, upOps.Count);
+
+ var alterColumnOperation = Assert.IsType(upOps[0]);
+ Assert.Equal(SqliteValueGenerationStrategy.Autoincrement, alterColumnOperation[SqliteAnnotationNames.ValueGenerationStrategy]);
+ Assert.Null(alterColumnOperation.OldColumn[SqliteAnnotationNames.ValueGenerationStrategy]);
+ });
+
+ [ConditionalFact]
+ public void No_repeated_alter_column_for_autoincrement_with_converter()
+ => Execute(
+ source => source.Entity(
+ x =>
+ {
+ x.Property(e => e.Id).HasConversion(
+ v => v.Value,
+ v => new ProductId(v));
+ x.HasKey(e => e.Id);
+ x.Property(e => e.Id).UseAutoincrement();
+ }),
+ target => target.Entity(
+ x =>
+ {
+ x.Property(e => e.Id).HasConversion(
+ v => v.Value,
+ v => new ProductId(v));
+ x.HasKey(e => e.Id);
+ x.Property(e => e.Id).UseAutoincrement();
+ }),
+ upOps =>
+ {
+ // Should have no operations since the models are the same
+ Assert.Empty(upOps);
+ });
+
+ protected override TestHelpers TestHelpers => SqliteTestHelpers.Instance;
+
+ // Test entities
+ public record struct ProductId(int Value);
+
+ public class ProductWithConverter
+ {
+ public ProductId Id { get; set; }
+ public required string Name { get; set; }
+ }
+}
\ No newline at end of file
From e84ae21b85ca3547487c2c519ba904fa0a348959 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 5 Sep 2025 23:43:52 +0000
Subject: [PATCH 08/39] Add test for migration consistency between string API
and convention with converters
Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
---
.../Migrations/SqliteModelDifferTest.cs | 26 +++++++++++++++++++
1 file changed, 26 insertions(+)
diff --git a/test/EFCore.Sqlite.Tests/Migrations/SqliteModelDifferTest.cs b/test/EFCore.Sqlite.Tests/Migrations/SqliteModelDifferTest.cs
index fd63e9eadef..0c5f081ede7 100644
--- a/test/EFCore.Sqlite.Tests/Migrations/SqliteModelDifferTest.cs
+++ b/test/EFCore.Sqlite.Tests/Migrations/SqliteModelDifferTest.cs
@@ -124,6 +124,32 @@ public void No_repeated_alter_column_for_autoincrement_with_converter()
Assert.Empty(upOps);
});
+ [ConditionalFact]
+ public void No_migration_operations_when_string_api_matches_convention_with_converter()
+ => Execute(
+ source => source.Entity(
+ "Product",
+ x =>
+ {
+ x.Property("Id");
+ x.HasKey("Id");
+ x.Property("Id").HasAnnotation(SqliteAnnotationNames.ValueGenerationStrategy, SqliteValueGenerationStrategy.Autoincrement);
+ }),
+ target => target.Entity(
+ x =>
+ {
+ x.Property(e => e.Id).HasConversion(
+ v => v.Value,
+ v => new ProductId(v));
+ x.HasKey(e => e.Id);
+ // No explicit UseAutoincrement() - should be set by convention
+ }),
+ upOps =>
+ {
+ // Should have no operations since both have autoincrement strategy
+ Assert.Empty(upOps);
+ });
+
protected override TestHelpers TestHelpers => SqliteTestHelpers.Instance;
// Test entities
From e0087b59e60d05f79f42ca02e9d700102e67019b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 6 Sep 2025 00:04:54 +0000
Subject: [PATCH 09/39] Add tests for SQLite annotation code generator in model
snapshots
Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
---
...arpMigrationsGeneratorModelSnapshotTest.cs | 157 ++++++++++++++++++
1 file changed, 157 insertions(+)
create mode 100644 test/EFCore.Sqlite.Tests/Design/SqliteCSharpMigrationsGeneratorModelSnapshotTest.cs
diff --git a/test/EFCore.Sqlite.Tests/Design/SqliteCSharpMigrationsGeneratorModelSnapshotTest.cs b/test/EFCore.Sqlite.Tests/Design/SqliteCSharpMigrationsGeneratorModelSnapshotTest.cs
new file mode 100644
index 00000000000..b9d0aa78d7a
--- /dev/null
+++ b/test/EFCore.Sqlite.Tests/Design/SqliteCSharpMigrationsGeneratorModelSnapshotTest.cs
@@ -0,0 +1,157 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.EntityFrameworkCore.Design.Internal;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata.Internal;
+using Microsoft.EntityFrameworkCore.Migrations.Design;
+using Microsoft.EntityFrameworkCore.Migrations.Internal;
+using Microsoft.EntityFrameworkCore.Sqlite.Design.Internal;
+using Microsoft.EntityFrameworkCore.Sqlite.Metadata.Internal;
+using Microsoft.EntityFrameworkCore.Sqlite.Storage.Internal;
+using Xunit.Sdk;
+
+namespace Microsoft.EntityFrameworkCore.Sqlite.Design;
+
+public class SqliteCSharpMigrationsGeneratorModelSnapshotTest
+{
+ [ConditionalFact]
+ public void Autoincrement_annotation_is_replaced_by_extension_method_call_in_snapshot()
+ {
+ Test(
+ builder =>
+ {
+ builder.Entity(e =>
+ {
+ e.Property(p => p.Id).UseAutoincrement();
+ });
+ },
+ "SqlitePropertyBuilderExtensions.UseAutoincrement(b.Property(\"Id\"));",
+ model =>
+ {
+ var entity = model.FindEntityType(typeof(EntityWithAutoincrement));
+ var property = entity!.FindProperty("Id");
+ Assert.Equal(SqliteValueGenerationStrategy.Autoincrement, property!.GetValueGenerationStrategy());
+ });
+ }
+
+ [ConditionalFact]
+ public void Autoincrement_works_with_value_converter_to_int()
+ {
+ Test(
+ builder =>
+ {
+ builder.Entity(e =>
+ {
+ e.Property(p => p.Id)
+ .HasConversion() // This should get autoincrement by convention
+ .UseAutoincrement(); // Explicitly set to test annotation generation
+ });
+ },
+ "SqlitePropertyBuilderExtensions.UseAutoincrement(b.Property(\"Id\"));",
+ model =>
+ {
+ var entity = model.FindEntityType(typeof(EntityWithConverterPk));
+ var property = entity!.FindProperty("Id");
+ // Should have autoincrement strategy even with value converter
+ Assert.Equal(SqliteValueGenerationStrategy.Autoincrement, property!.GetValueGenerationStrategy());
+ });
+ }
+
+ [ConditionalFact]
+ public void No_autoincrement_method_call_when_strategy_is_none()
+ {
+ Test(
+ builder =>
+ {
+ builder.Entity(e =>
+ {
+ // String primary key should not get autoincrement
+ });
+ },
+ "b.Property(\"Id\")", // Check that string property is generated but no UseAutoincrement call
+ model =>
+ {
+ var entity = model.FindEntityType(typeof(EntityWithStringKey));
+ var property = entity!.FindProperty("Id");
+ Assert.Equal(SqliteValueGenerationStrategy.None, property!.GetValueGenerationStrategy());
+ });
+
+ // Also verify that the UseAutoincrement call is NOT in the generated code
+ var modelBuilder = CreateConventionalModelBuilder();
+ modelBuilder.Model.RemoveAnnotation(CoreAnnotationNames.ProductVersion);
+ modelBuilder.Entity(e =>
+ {
+ // String primary key should not get autoincrement
+ });
+
+ var model = modelBuilder.FinalizeModel(designTime: true);
+ var generator = CreateMigrationsGenerator();
+ var code = generator.GenerateSnapshot("RootNamespace", typeof(DbContext), "Snapshot", model);
+
+ Assert.DoesNotContain("SqlitePropertyBuilderExtensions.UseAutoincrement", code);
+ }
+
+ private class EntityWithAutoincrement
+ {
+ public int Id { get; set; }
+ }
+
+ private class EntityWithConverterPk
+ {
+ public long Id { get; set; }
+ }
+
+ private class EntityWithStringKey
+ {
+ public string Id { get; set; } = null!;
+ }
+
+
+ protected void Test(Action buildModel, string expectedCodeFragment, Action assert)
+ {
+ var modelBuilder = CreateConventionalModelBuilder();
+ modelBuilder.Model.RemoveAnnotation(CoreAnnotationNames.ProductVersion);
+ buildModel(modelBuilder);
+
+ var model = modelBuilder.FinalizeModel(designTime: true);
+
+ var generator = CreateMigrationsGenerator();
+ var code = generator.GenerateSnapshot("RootNamespace", typeof(DbContext), "Snapshot", model);
+
+ assert(model);
+
+ // Check that the generated code contains the expected fragment
+ Assert.Contains(expectedCodeFragment, code);
+ }
+
+ protected SqliteTestHelpers.TestModelBuilder CreateConventionalModelBuilder()
+ => SqliteTestHelpers.Instance.CreateConventionBuilder();
+
+ protected CSharpMigrationsGenerator CreateMigrationsGenerator()
+ {
+ var sqliteTypeMappingSource = new SqliteTypeMappingSource(
+ TestServiceFactory.Instance.Create(),
+ TestServiceFactory.Instance.Create());
+
+ var codeHelper = new CSharpHelper(sqliteTypeMappingSource);
+
+ var sqliteAnnotationCodeGenerator = new SqliteAnnotationCodeGenerator(
+ new AnnotationCodeGeneratorDependencies(sqliteTypeMappingSource));
+
+ var generator = new CSharpMigrationsGenerator(
+ new MigrationsCodeGeneratorDependencies(
+ sqliteTypeMappingSource,
+ sqliteAnnotationCodeGenerator),
+ new CSharpMigrationsGeneratorDependencies(
+ codeHelper,
+ new CSharpMigrationOperationGenerator(
+ new CSharpMigrationOperationGeneratorDependencies(
+ codeHelper)),
+ new CSharpSnapshotGenerator(
+ new CSharpSnapshotGeneratorDependencies(
+ codeHelper, sqliteTypeMappingSource, sqliteAnnotationCodeGenerator))));
+
+ return generator;
+ }
+}
\ No newline at end of file
From da1ebe386b3191217922f816302c6fcc5c8102cf Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 6 Sep 2025 00:22:18 +0000
Subject: [PATCH 10/39] Address review feedback: fix annotation code generator
logic, remove redundant comments, and use terser code
Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
---
.../Internal/SqliteAnnotationCodeGenerator.cs | 17 ++++++++-----
.../Extensions/SqlitePropertyExtensions.cs | 25 ++++++-------------
.../Internal/SqliteAnnotationProvider.cs | 1 -
3 files changed, 18 insertions(+), 25 deletions(-)
diff --git a/src/EFCore.Sqlite.Core/Design/Internal/SqliteAnnotationCodeGenerator.cs b/src/EFCore.Sqlite.Core/Design/Internal/SqliteAnnotationCodeGenerator.cs
index 13114613720..f8c4c25bc21 100644
--- a/src/EFCore.Sqlite.Core/Design/Internal/SqliteAnnotationCodeGenerator.cs
+++ b/src/EFCore.Sqlite.Core/Design/Internal/SqliteAnnotationCodeGenerator.cs
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Diagnostics.CodeAnalysis;
using Microsoft.EntityFrameworkCore.Sqlite.Metadata.Internal;
namespace Microsoft.EntityFrameworkCore.Sqlite.Design.Internal;
@@ -48,7 +49,7 @@ public override IReadOnlyList GenerateFluentApiCalls(
{
var fragments = new List(base.GenerateFluentApiCalls(property, annotations));
- if (GetAndRemove(annotations, SqliteAnnotationNames.ValueGenerationStrategy) is { } strategy
+ if (TryGetAndRemove(annotations, SqliteAnnotationNames.ValueGenerationStrategy, out var strategy)
&& strategy == SqliteValueGenerationStrategy.Autoincrement)
{
var methodInfo = property.DeclaringType is IComplexType
@@ -70,22 +71,26 @@ protected override bool IsHandledByConvention(IProperty property, IAnnotation an
{
if (annotation.Name == SqliteAnnotationNames.ValueGenerationStrategy)
{
- // Autoincrement strategy is handled by convention for single-column integer primary keys
- return (SqliteValueGenerationStrategy)annotation.Value! == SqliteValueGenerationStrategy.None;
+ return (SqliteValueGenerationStrategy)annotation.Value! == SqlitePropertyExtensions.GetDefaultValueGenerationStrategy(property);
}
return base.IsHandledByConvention(property, annotation);
}
- private static T? GetAndRemove(IDictionary annotations, string annotationName)
+ private static bool TryGetAndRemove(
+ IDictionary annotations,
+ string annotationName,
+ [NotNullWhen(true)] out T? annotationValue)
{
if (annotations.TryGetValue(annotationName, out var annotation)
&& annotation.Value != null)
{
annotations.Remove(annotationName);
- return (T)annotation.Value;
+ annotationValue = (T)annotation.Value;
+ return true;
}
- return default;
+ annotationValue = default;
+ return false;
}
}
\ No newline at end of file
diff --git a/src/EFCore.Sqlite.Core/Extensions/SqlitePropertyExtensions.cs b/src/EFCore.Sqlite.Core/Extensions/SqlitePropertyExtensions.cs
index 095b75ff5d3..059fab68463 100644
--- a/src/EFCore.Sqlite.Core/Extensions/SqlitePropertyExtensions.cs
+++ b/src/EFCore.Sqlite.Core/Extensions/SqlitePropertyExtensions.cs
@@ -21,15 +21,9 @@ public static class SqlitePropertyExtensions
/// The property.
/// The strategy to use for the property.
public static SqliteValueGenerationStrategy GetValueGenerationStrategy(this IReadOnlyProperty property)
- {
- var annotation = property[SqliteAnnotationNames.ValueGenerationStrategy];
- if (annotation != null)
- {
- return (SqliteValueGenerationStrategy)annotation;
- }
-
- return GetDefaultValueGenerationStrategy(property);
- }
+ => property[SqliteAnnotationNames.ValueGenerationStrategy] is SqliteValueGenerationStrategy strategy
+ ? strategy
+ : GetDefaultValueGenerationStrategy(property);
///
/// Returns the to use for the property.
@@ -53,18 +47,13 @@ public static SqliteValueGenerationStrategy GetValueGenerationStrategy(
: GetDefaultValueGenerationStrategy(property);
}
- private static SqliteValueGenerationStrategy GetDefaultValueGenerationStrategy(IReadOnlyProperty property)
+ internal static SqliteValueGenerationStrategy GetDefaultValueGenerationStrategy(IReadOnlyProperty property)
{
- // Return None if default value, default value sql, or computed value are set
+ // Return None if default value, default value sql, computed value are set, or the property is part of a foreign key
if (property.TryGetDefaultValue(out _)
|| property.GetDefaultValueSql() != null
- || property.GetComputedColumnSql() != null)
- {
- return SqliteValueGenerationStrategy.None;
- }
-
- // Return None if the property is part of a foreign key
- if (property.IsForeignKey())
+ || property.GetComputedColumnSql() != null
+ || property.IsForeignKey())
{
return SqliteValueGenerationStrategy.None;
}
diff --git a/src/EFCore.Sqlite.Core/Metadata/Internal/SqliteAnnotationProvider.cs b/src/EFCore.Sqlite.Core/Metadata/Internal/SqliteAnnotationProvider.cs
index 9bbe831df1b..bb8376cb1d0 100644
--- a/src/EFCore.Sqlite.Core/Metadata/Internal/SqliteAnnotationProvider.cs
+++ b/src/EFCore.Sqlite.Core/Metadata/Internal/SqliteAnnotationProvider.cs
@@ -66,7 +66,6 @@ public override IEnumerable For(IColumn column, bool designTime)
// Model validation ensures that these facets are the same on all mapped properties
var property = column.PropertyMappings.First().Property;
- // Use the strategy-based approach to determine AUTOINCREMENT
if (property.GetValueGenerationStrategy() == SqliteValueGenerationStrategy.Autoincrement)
{
yield return new Annotation(SqliteAnnotationNames.Autoincrement, true);
From ca03cda600d2b94e46eb80430e4a996c1d0c9e78 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 6 Sep 2025 00:34:36 +0000
Subject: [PATCH 11/39] Address review feedback: rename test class, make
GetDefaultValueGenerationStrategy public, update convention logic, and remove
redundant test
Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
---
.../Extensions/SqlitePropertyExtensions.cs | 12 +++++--
.../SqliteValueGenerationConvention.cs | 33 -------------------
.../Migrations/MigrationsSqliteTest.cs | 30 -----------------
...arpMigrationsGeneratorModelSnapshotTest.cs | 10 +++---
4 files changed, 14 insertions(+), 71 deletions(-)
diff --git a/src/EFCore.Sqlite.Core/Extensions/SqlitePropertyExtensions.cs b/src/EFCore.Sqlite.Core/Extensions/SqlitePropertyExtensions.cs
index 059fab68463..c94bfc45f59 100644
--- a/src/EFCore.Sqlite.Core/Extensions/SqlitePropertyExtensions.cs
+++ b/src/EFCore.Sqlite.Core/Extensions/SqlitePropertyExtensions.cs
@@ -47,13 +47,19 @@ public static SqliteValueGenerationStrategy GetValueGenerationStrategy(
: GetDefaultValueGenerationStrategy(property);
}
- internal static SqliteValueGenerationStrategy GetDefaultValueGenerationStrategy(IReadOnlyProperty property)
+ ///
+ /// Returns the default to use for the property.
+ ///
+ /// The property.
+ /// The default strategy for the property.
+ public static SqliteValueGenerationStrategy GetDefaultValueGenerationStrategy(IReadOnlyProperty property)
{
- // Return None if default value, default value sql, computed value are set, or the property is part of a foreign key
+ // Return None if default value, default value sql, computed value are set, the property is part of a foreign key, or ValueGenerated is Never
if (property.TryGetDefaultValue(out _)
|| property.GetDefaultValueSql() != null
|| property.GetComputedColumnSql() != null
- || property.IsForeignKey())
+ || property.IsForeignKey()
+ || property.ValueGenerated == ValueGenerated.Never)
{
return SqliteValueGenerationStrategy.None;
}
diff --git a/src/EFCore.Sqlite.Core/Metadata/Conventions/SqliteValueGenerationConvention.cs b/src/EFCore.Sqlite.Core/Metadata/Conventions/SqliteValueGenerationConvention.cs
index ec81d7670b7..cecdf23dc04 100644
--- a/src/EFCore.Sqlite.Core/Metadata/Conventions/SqliteValueGenerationConvention.cs
+++ b/src/EFCore.Sqlite.Core/Metadata/Conventions/SqliteValueGenerationConvention.cs
@@ -48,37 +48,4 @@ public SqliteValueGenerationConvention(
return base.GetValueGenerated(property);
}
-
- ///
- /// Returns the default value generation strategy for the property.
- ///
- /// The property.
- /// The default strategy for the property.
- private static SqliteValueGenerationStrategy GetDefaultValueGenerationStrategy(IConventionProperty property)
- {
- // Return None if default value, default value sql, or computed value are set
- if (property.TryGetDefaultValue(out _)
- || property.GetDefaultValueSql() != null
- || property.GetComputedColumnSql() != null)
- {
- return SqliteValueGenerationStrategy.None;
- }
-
- // Return None if the property is part of a foreign key
- if (property.IsForeignKey())
- {
- return SqliteValueGenerationStrategy.None;
- }
-
- var entityType = (IConventionEntityType)property.DeclaringType;
- var primaryKey = entityType.FindPrimaryKey();
- if (primaryKey is { Properties.Count: 1 }
- && primaryKey.Properties[0] == property
- && property.ClrType.UnwrapNullableType().IsInteger())
- {
- return SqliteValueGenerationStrategy.Autoincrement;
- }
-
- return SqliteValueGenerationStrategy.None;
- }
}
\ No newline at end of file
diff --git a/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsSqliteTest.cs
index b68911714a8..a1777bd08ac 100644
--- a/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsSqliteTest.cs
+++ b/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsSqliteTest.cs
@@ -2348,37 +2348,7 @@ await Test(
""");
}
- [ConditionalFact]
- public virtual async Task Create_table_with_autoincrement_column()
- {
- await Test(
- builder => { },
- builder => builder.Entity(
- "Product",
- x =>
- {
- x.Property("Id").UseAutoincrement();
- x.HasKey("Id");
- x.Property("Name");
- }),
- model =>
- {
- var table = Assert.Single(model.Tables);
- Assert.Equal("Product", table.Name);
- Assert.Equal(2, table.Columns.Count());
-
- var idColumn = Assert.Single(table.Columns, c => c.Name == "Id");
- Assert.False(idColumn.IsNullable);
- });
- AssertSql(
- """
-CREATE TABLE "Product" (
- "Id" INTEGER NOT NULL CONSTRAINT "PK_Product" PRIMARY KEY AUTOINCREMENT,
- "Name" TEXT NULL
-);
-""");
- }
[ConditionalFact]
public virtual async Task Create_table_with_autoincrement_and_value_converter()
diff --git a/test/EFCore.Sqlite.Tests/Design/SqliteCSharpMigrationsGeneratorModelSnapshotTest.cs b/test/EFCore.Sqlite.Tests/Design/SqliteCSharpMigrationsGeneratorModelSnapshotTest.cs
index b9d0aa78d7a..9a8620605e2 100644
--- a/test/EFCore.Sqlite.Tests/Design/SqliteCSharpMigrationsGeneratorModelSnapshotTest.cs
+++ b/test/EFCore.Sqlite.Tests/Design/SqliteCSharpMigrationsGeneratorModelSnapshotTest.cs
@@ -13,7 +13,7 @@
namespace Microsoft.EntityFrameworkCore.Sqlite.Design;
-public class SqliteCSharpMigrationsGeneratorModelSnapshotTest
+public class CSharpMigrationsGeneratorModelSnapshotSqliteTest
{
[ConditionalFact]
public void Autoincrement_annotation_is_replaced_by_extension_method_call_in_snapshot()
@@ -64,15 +64,15 @@ public void No_autoincrement_method_call_when_strategy_is_none()
Test(
builder =>
{
- builder.Entity(e =>
+ builder.Entity(e =>
{
- // String primary key should not get autoincrement
+ e.Property(p => p.Id).ValueGeneratedNever();
});
},
- "b.Property(\"Id\")", // Check that string property is generated but no UseAutoincrement call
+ "b.Property(\"Id\")", // Check that int property is generated but no UseAutoincrement call
model =>
{
- var entity = model.FindEntityType(typeof(EntityWithStringKey));
+ var entity = model.FindEntityType(typeof(EntityWithAutoincrement));
var property = entity!.FindProperty("Id");
Assert.Equal(SqliteValueGenerationStrategy.None, property!.GetValueGenerationStrategy());
});
From fdb1536e157038567ae5e06bfb98d5a4a3b861ba Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 6 Sep 2025 00:48:46 +0000
Subject: [PATCH 12/39] Address review feedback: remove redundant test, make
GetDefaultValueGenerationStrategy extension method, remove comments
Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
---
.../Internal/SqliteAnnotationCodeGenerator.cs | 2 +-
.../Extensions/SqlitePropertyExtensions.cs | 7 +--
.../Migrations/MigrationsSqliteTest.cs | 59 -------------------
.../Scaffolding/CompiledModelSqliteTest.cs | 1 -
4 files changed, 4 insertions(+), 65 deletions(-)
diff --git a/src/EFCore.Sqlite.Core/Design/Internal/SqliteAnnotationCodeGenerator.cs b/src/EFCore.Sqlite.Core/Design/Internal/SqliteAnnotationCodeGenerator.cs
index f8c4c25bc21..7ddd3295de5 100644
--- a/src/EFCore.Sqlite.Core/Design/Internal/SqliteAnnotationCodeGenerator.cs
+++ b/src/EFCore.Sqlite.Core/Design/Internal/SqliteAnnotationCodeGenerator.cs
@@ -71,7 +71,7 @@ protected override bool IsHandledByConvention(IProperty property, IAnnotation an
{
if (annotation.Name == SqliteAnnotationNames.ValueGenerationStrategy)
{
- return (SqliteValueGenerationStrategy)annotation.Value! == SqlitePropertyExtensions.GetDefaultValueGenerationStrategy(property);
+ return (SqliteValueGenerationStrategy)annotation.Value! == property.GetDefaultValueGenerationStrategy();
}
return base.IsHandledByConvention(property, annotation);
diff --git a/src/EFCore.Sqlite.Core/Extensions/SqlitePropertyExtensions.cs b/src/EFCore.Sqlite.Core/Extensions/SqlitePropertyExtensions.cs
index c94bfc45f59..9efd6789029 100644
--- a/src/EFCore.Sqlite.Core/Extensions/SqlitePropertyExtensions.cs
+++ b/src/EFCore.Sqlite.Core/Extensions/SqlitePropertyExtensions.cs
@@ -23,7 +23,7 @@ public static class SqlitePropertyExtensions
public static SqliteValueGenerationStrategy GetValueGenerationStrategy(this IReadOnlyProperty property)
=> property[SqliteAnnotationNames.ValueGenerationStrategy] is SqliteValueGenerationStrategy strategy
? strategy
- : GetDefaultValueGenerationStrategy(property);
+ : property.GetDefaultValueGenerationStrategy();
///
/// Returns the to use for the property.
@@ -44,7 +44,7 @@ public static SqliteValueGenerationStrategy GetValueGenerationStrategy(
var sharedProperty = property.FindSharedStoreObjectRootProperty(storeObject);
return sharedProperty != null
? sharedProperty.GetValueGenerationStrategy(storeObject)
- : GetDefaultValueGenerationStrategy(property);
+ : property.GetDefaultValueGenerationStrategy();
}
///
@@ -52,9 +52,8 @@ public static SqliteValueGenerationStrategy GetValueGenerationStrategy(
///
/// The property.
/// The default strategy for the property.
- public static SqliteValueGenerationStrategy GetDefaultValueGenerationStrategy(IReadOnlyProperty property)
+ public static SqliteValueGenerationStrategy GetDefaultValueGenerationStrategy(this IReadOnlyProperty property)
{
- // Return None if default value, default value sql, computed value are set, the property is part of a foreign key, or ValueGenerated is Never
if (property.TryGetDefaultValue(out _)
|| property.GetDefaultValueSql() != null
|| property.GetComputedColumnSql() != null
diff --git a/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsSqliteTest.cs
index a1777bd08ac..739ebffa689 100644
--- a/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsSqliteTest.cs
+++ b/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsSqliteTest.cs
@@ -2401,66 +2401,7 @@ await AssertNotSupportedAsync(
"SQLite AUTOINCREMENT can only be used with a single primary key column.");
}
- [ConditionalFact]
- public virtual async Task Alter_column_add_autoincrement()
- {
- await Test(
- builder => builder.Entity(
- "Product",
- x =>
- {
- x.Property("Id");
- x.HasKey("Id");
- x.Property("Name");
- }),
- builder => builder.Entity(
- "Product",
- x =>
- {
- x.Property("Id").UseAutoincrement();
- x.HasKey("Id");
- x.Property("Name");
- }),
- model =>
- {
- var table = Assert.Single(model.Tables);
- Assert.Equal("Product", table.Name);
- Assert.Equal(2, table.Columns.Count());
-
- var idColumn = Assert.Single(table.Columns, c => c.Name == "Id");
- Assert.False(idColumn.IsNullable);
- });
- AssertSql(
- """
-CREATE TABLE "ef_temp_Product" (
- "Id" INTEGER NOT NULL CONSTRAINT "PK_Product" PRIMARY KEY AUTOINCREMENT,
- "Name" TEXT NULL
-);
-""",
- //
- """
-INSERT INTO "ef_temp_Product" ("Name")
-SELECT "Name"
-FROM "Product";
-""",
- //
- """
-PRAGMA foreign_keys = 0;
-""",
- //
- """
-DROP TABLE "Product";
-""",
- //
- """
-ALTER TABLE "ef_temp_Product" RENAME TO "Product";
-""",
- //
- """
-PRAGMA foreign_keys = 1;
-""");
- }
[ConditionalFact]
public virtual async Task Alter_column_remove_autoincrement()
diff --git a/test/EFCore.Sqlite.FunctionalTests/Scaffolding/CompiledModelSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Scaffolding/CompiledModelSqliteTest.cs
index 9a0a4b34681..5b405ec9197 100644
--- a/test/EFCore.Sqlite.FunctionalTests/Scaffolding/CompiledModelSqliteTest.cs
+++ b/test/EFCore.Sqlite.FunctionalTests/Scaffolding/CompiledModelSqliteTest.cs
@@ -31,7 +31,6 @@ protected override void BuildBigModel(ModelBuilder modelBuilder, bool jsonColumn
{
eb.Property("Id");
eb.HasKey("Id");
- // This should be auto-configured by convention to use AUTOINCREMENT
eb.Property("Name");
});
From 665a9253baebab98f1af01590d448c1d2c3ac0ba Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 6 Sep 2025 01:07:19 +0000
Subject: [PATCH 13/39] Address review feedback: rename test class, remove
comments and empty lines, fix assertions
Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
---
.../Migrations/MigrationsSqliteTest.cs | 1 -
...arpMigrationsGeneratorModelSnapshotTest.cs | 24 ++++---------------
.../SqliteMetadataExtensionsTest.cs | 2 ++
3 files changed, 6 insertions(+), 21 deletions(-)
diff --git a/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsSqliteTest.cs
index 739ebffa689..dbc575fae9b 100644
--- a/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsSqliteTest.cs
+++ b/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsSqliteTest.cs
@@ -2349,7 +2349,6 @@ await Test(
}
-
[ConditionalFact]
public virtual async Task Create_table_with_autoincrement_and_value_converter()
{
diff --git a/test/EFCore.Sqlite.Tests/Design/SqliteCSharpMigrationsGeneratorModelSnapshotTest.cs b/test/EFCore.Sqlite.Tests/Design/SqliteCSharpMigrationsGeneratorModelSnapshotTest.cs
index 9a8620605e2..1d1a00b0f74 100644
--- a/test/EFCore.Sqlite.Tests/Design/SqliteCSharpMigrationsGeneratorModelSnapshotTest.cs
+++ b/test/EFCore.Sqlite.Tests/Design/SqliteCSharpMigrationsGeneratorModelSnapshotTest.cs
@@ -13,7 +13,7 @@
namespace Microsoft.EntityFrameworkCore.Sqlite.Design;
-public class CSharpMigrationsGeneratorModelSnapshotSqliteTest
+public class CSharpMigrationsGeneratorSqliteTest
{
[ConditionalFact]
public void Autoincrement_annotation_is_replaced_by_extension_method_call_in_snapshot()
@@ -44,8 +44,8 @@ public void Autoincrement_works_with_value_converter_to_int()
builder.Entity(e =>
{
e.Property(p => p.Id)
- .HasConversion() // This should get autoincrement by convention
- .UseAutoincrement(); // Explicitly set to test annotation generation
+ .HasConversion()
+ .UseAutoincrement();
});
},
"SqlitePropertyBuilderExtensions.UseAutoincrement(b.Property(\"Id\"));",
@@ -69,27 +69,13 @@ public void No_autoincrement_method_call_when_strategy_is_none()
e.Property(p => p.Id).ValueGeneratedNever();
});
},
- "b.Property(\"Id\")", // Check that int property is generated but no UseAutoincrement call
+ "b.Property(\"Id\")",
model =>
{
var entity = model.FindEntityType(typeof(EntityWithAutoincrement));
var property = entity!.FindProperty("Id");
Assert.Equal(SqliteValueGenerationStrategy.None, property!.GetValueGenerationStrategy());
});
-
- // Also verify that the UseAutoincrement call is NOT in the generated code
- var modelBuilder = CreateConventionalModelBuilder();
- modelBuilder.Model.RemoveAnnotation(CoreAnnotationNames.ProductVersion);
- modelBuilder.Entity(e =>
- {
- // String primary key should not get autoincrement
- });
-
- var model = modelBuilder.FinalizeModel(designTime: true);
- var generator = CreateMigrationsGenerator();
- var code = generator.GenerateSnapshot("RootNamespace", typeof(DbContext), "Snapshot", model);
-
- Assert.DoesNotContain("SqlitePropertyBuilderExtensions.UseAutoincrement", code);
}
private class EntityWithAutoincrement
@@ -107,7 +93,6 @@ private class EntityWithStringKey
public string Id { get; set; } = null!;
}
-
protected void Test(Action buildModel, string expectedCodeFragment, Action assert)
{
var modelBuilder = CreateConventionalModelBuilder();
@@ -121,7 +106,6 @@ protected void Test(Action buildModel, string expectedCodeFragment
assert(model);
- // Check that the generated code contains the expected fragment
Assert.Contains(expectedCodeFragment, code);
}
diff --git a/test/EFCore.Sqlite.Tests/Extensions/SqliteMetadataExtensionsTest.cs b/test/EFCore.Sqlite.Tests/Extensions/SqliteMetadataExtensionsTest.cs
index a0e6080ded1..55cfb9f0da0 100644
--- a/test/EFCore.Sqlite.Tests/Extensions/SqliteMetadataExtensionsTest.cs
+++ b/test/EFCore.Sqlite.Tests/Extensions/SqliteMetadataExtensionsTest.cs
@@ -92,6 +92,8 @@ public void Generic_UseAutoincrement_sets_value_generation_strategy()
Assert.Equal(SqliteValueGenerationStrategy.Autoincrement, propertyBuilder.Metadata.GetValueGenerationStrategy());
}
+
+
[ConditionalFact]
public void Default_value_generation_strategy_for_integer_primary_key()
{
From 219a91c725b7623945cf76cc469081f0efb1797b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 6 Sep 2025 01:31:22 +0000
Subject: [PATCH 14/39] Address review feedback: fix ambiguous
GetValueGenerationStrategy calls and improve test structure
Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
---
...rpMigrationsGeneratorTest.ModelSnapshot.cs | 26 +++++++++----------
.../Design/SnapshotModelProcessorTest.cs | 8 +++---
.../Internal/CSharpDbContextGeneratorTest.cs | 2 +-
.../Migrations/SqliteModelDifferTest.cs | 18 ++++---------
4 files changed, 23 insertions(+), 31 deletions(-)
diff --git a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs
index 53ee02df81c..94a3638a136 100644
--- a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs
+++ b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs
@@ -694,10 +694,10 @@ public virtual void Model_Fluent_APIs_are_properly_generated()
"""),
o =>
{
- Assert.Equal(SqlServerValueGenerationStrategy.SequenceHiLo, o.GetValueGenerationStrategy());
+ Assert.Equal(SqlServerValueGenerationStrategy.SequenceHiLo, Microsoft.EntityFrameworkCore.SqlServerPropertyExtensions.GetValueGenerationStrategy(o.GetEntityTypes().Single().GetProperty("Id")));
Assert.Equal(
SqlServerValueGenerationStrategy.SequenceHiLo,
- o.GetEntityTypes().Single().GetProperty("Id").GetValueGenerationStrategy());
+ Microsoft.EntityFrameworkCore.SqlServerPropertyExtensions.GetValueGenerationStrategy(o.GetEntityTypes().Single().GetProperty("Id")));
});
[ConditionalFact]
@@ -735,10 +735,10 @@ public virtual void Model_fluent_APIs_for_sequence_key_are_properly_generated()
"""),
o =>
{
- Assert.Equal(SqlServerValueGenerationStrategy.Sequence, o.GetValueGenerationStrategy());
+ Assert.Equal(SqlServerValueGenerationStrategy.Sequence, Microsoft.EntityFrameworkCore.SqlServerPropertyExtensions.GetValueGenerationStrategy(o.GetEntityTypes().Single().GetProperty("Id")));
Assert.Equal(
SqlServerValueGenerationStrategy.Sequence,
- o.GetEntityTypes().Single().GetProperty("Id").GetValueGenerationStrategy());
+ Microsoft.EntityFrameworkCore.SqlServerPropertyExtensions.GetValueGenerationStrategy(o.GetEntityTypes().Single().GetProperty("Id")));
});
[ConditionalFact]
@@ -1569,12 +1569,12 @@ public virtual void Entity_splitting_is_stored_in_snapshot_with_tables()
Assert.Equal(nameof(Order), orderEntityType.GetTableName());
var id = orderEntityType.FindProperty("Id");
- Assert.Equal(SqlServerValueGenerationStrategy.IdentityColumn, id.GetValueGenerationStrategy());
+ Assert.Equal(SqlServerValueGenerationStrategy.IdentityColumn, Microsoft.EntityFrameworkCore.SqlServerPropertyExtensions.GetValueGenerationStrategy(id));
Assert.Equal(1, id.GetIdentitySeed());
Assert.Equal(1, id.GetIdentityIncrement());
var overrides = id.FindOverrides(StoreObjectIdentifier.Create(orderEntityType, StoreObjectType.Table).Value)!;
- Assert.Equal(SqlServerValueGenerationStrategy.IdentityColumn, overrides.GetValueGenerationStrategy());
+ Assert.Equal(SqlServerValueGenerationStrategy.IdentityColumn, Microsoft.EntityFrameworkCore.SqlServerPropertyExtensions.GetValueGenerationStrategy(overrides));
Assert.Equal(2, overrides.GetIdentitySeed());
Assert.Equal(3, overrides.GetIdentityIncrement());
Assert.Equal("arr", overrides["fii"]);
@@ -2255,7 +2255,7 @@ public virtual void Model_use_identity_columns_custom_seed_increment()
Assert.Equal(5, o.GetIdentityIncrement());
var property = o.FindEntityType("Building").FindProperty("Id");
- Assert.Equal(SqlServerValueGenerationStrategy.IdentityColumn, property.GetValueGenerationStrategy());
+ Assert.Equal(SqlServerValueGenerationStrategy.IdentityColumn, Microsoft.EntityFrameworkCore.SqlServerPropertyExtensions.GetValueGenerationStrategy(property));
Assert.Equal(long.MaxValue, property.GetIdentitySeed());
Assert.Equal(5, property.GetIdentityIncrement());
});
@@ -4628,10 +4628,10 @@ public virtual void Property_ValueGenerated_non_identity()
{
var id = model.GetEntityTypes().Single().GetProperty(nameof(EntityWithEnumType.Id));
Assert.Equal(ValueGenerated.OnAdd, id.ValueGenerated);
- Assert.Equal(SqlServerValueGenerationStrategy.None, id.GetValueGenerationStrategy());
+ Assert.Equal(SqlServerValueGenerationStrategy.None, Microsoft.EntityFrameworkCore.SqlServerPropertyExtensions.GetValueGenerationStrategy(id));
var day = model.GetEntityTypes().Single().GetProperty(nameof(EntityWithEnumType.Day));
Assert.Equal(ValueGenerated.OnAdd, day.ValueGenerated);
- Assert.Equal(SqlServerValueGenerationStrategy.None, day.GetValueGenerationStrategy());
+ Assert.Equal(SqlServerValueGenerationStrategy.None, Microsoft.EntityFrameworkCore.SqlServerPropertyExtensions.GetValueGenerationStrategy(day));
});
[ConditionalFact]
@@ -5724,7 +5724,7 @@ public virtual void Property_with_identity_column()
o =>
{
var property = o.FindEntityType("Building").FindProperty("Id");
- Assert.Equal(SqlServerValueGenerationStrategy.IdentityColumn, property.GetValueGenerationStrategy());
+ Assert.Equal(SqlServerValueGenerationStrategy.IdentityColumn, Microsoft.EntityFrameworkCore.SqlServerPropertyExtensions.GetValueGenerationStrategy(property));
Assert.Equal(1, property.GetIdentitySeed());
Assert.Equal(1, property.GetIdentityIncrement());
});
@@ -5763,7 +5763,7 @@ public virtual void Property_with_identity_column_custom_seed()
o =>
{
var property = o.FindEntityType("Building").FindProperty("Id");
- Assert.Equal(SqlServerValueGenerationStrategy.IdentityColumn, property.GetValueGenerationStrategy());
+ Assert.Equal(SqlServerValueGenerationStrategy.IdentityColumn, Microsoft.EntityFrameworkCore.SqlServerPropertyExtensions.GetValueGenerationStrategy(property));
Assert.Equal(5, property.GetIdentitySeed());
Assert.Equal(1, property.GetIdentityIncrement());
});
@@ -5802,7 +5802,7 @@ public virtual void Property_with_identity_column_custom_increment()
o =>
{
var property = o.FindEntityType("Building").FindProperty("Id");
- Assert.Equal(SqlServerValueGenerationStrategy.IdentityColumn, property.GetValueGenerationStrategy());
+ Assert.Equal(SqlServerValueGenerationStrategy.IdentityColumn, Microsoft.EntityFrameworkCore.SqlServerPropertyExtensions.GetValueGenerationStrategy(property));
Assert.Equal(1, property.GetIdentitySeed());
Assert.Equal(5, property.GetIdentityIncrement());
});
@@ -5841,7 +5841,7 @@ public virtual void Property_with_identity_column_custom_seed_increment()
o =>
{
var property = o.FindEntityType("Building").FindProperty("Id");
- Assert.Equal(SqlServerValueGenerationStrategy.IdentityColumn, property.GetValueGenerationStrategy());
+ Assert.Equal(SqlServerValueGenerationStrategy.IdentityColumn, Microsoft.EntityFrameworkCore.SqlServerPropertyExtensions.GetValueGenerationStrategy(property));
Assert.Equal(5, property.GetIdentitySeed());
Assert.Equal(5, property.GetIdentityIncrement());
});
diff --git a/test/EFCore.Design.Tests/Migrations/Design/SnapshotModelProcessorTest.cs b/test/EFCore.Design.Tests/Migrations/Design/SnapshotModelProcessorTest.cs
index afd2e2cbaa5..0b213ad4f20 100644
--- a/test/EFCore.Design.Tests/Migrations/Design/SnapshotModelProcessorTest.cs
+++ b/test/EFCore.Design.Tests/Migrations/Design/SnapshotModelProcessorTest.cs
@@ -262,15 +262,15 @@ private static IModel PreprocessModel(ModelSnapshot snapshot)
property.SetValueGenerated(null, ConfigurationSource.Explicit);
}
- if (property.GetValueGenerationStrategy() != SqlServerValueGenerationStrategy.None)
+ if (Microsoft.EntityFrameworkCore.SqlServerPropertyExtensions.GetValueGenerationStrategy(property) != SqlServerValueGenerationStrategy.None)
{
- property.SetValueGenerationStrategy(null);
+ Microsoft.EntityFrameworkCore.SqlServerPropertyExtensions.SetValueGenerationStrategy(property, null);
}
}
- else if (property.GetValueGenerationStrategy() is var strategy
+ else if (Microsoft.EntityFrameworkCore.SqlServerPropertyExtensions.GetValueGenerationStrategy(property) is var strategy
&& strategy != SqlServerValueGenerationStrategy.None)
{
- property.SetValueGenerationStrategy(strategy);
+ Microsoft.EntityFrameworkCore.SqlServerPropertyExtensions.SetValueGenerationStrategy(property, strategy);
}
}
}
diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpDbContextGeneratorTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpDbContextGeneratorTest.cs
index 8d2afa05a25..0d3392faeee 100644
--- a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpDbContextGeneratorTest.cs
+++ b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpDbContextGeneratorTest.cs
@@ -1330,7 +1330,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
{
var entityType = Assert.Single(model.GetEntityTypes());
var property = Assert.Single(entityType.GetProperties());
- Assert.Equal(SqlServerValueGenerationStrategy.None, property.GetValueGenerationStrategy());
+ Assert.Equal(SqlServerValueGenerationStrategy.None, Microsoft.EntityFrameworkCore.SqlServerPropertyExtensions.GetValueGenerationStrategy(property));
});
[ConditionalTheory, InlineData(false), InlineData(true)]
diff --git a/test/EFCore.Sqlite.Tests/Migrations/SqliteModelDifferTest.cs b/test/EFCore.Sqlite.Tests/Migrations/SqliteModelDifferTest.cs
index 0c5f081ede7..ca54b9a2ec1 100644
--- a/test/EFCore.Sqlite.Tests/Migrations/SqliteModelDifferTest.cs
+++ b/test/EFCore.Sqlite.Tests/Migrations/SqliteModelDifferTest.cs
@@ -100,16 +100,7 @@ public void Autoincrement_with_value_converter_generates_consistent_migrations()
[ConditionalFact]
public void No_repeated_alter_column_for_autoincrement_with_converter()
=> Execute(
- source => source.Entity(
- x =>
- {
- x.Property(e => e.Id).HasConversion(
- v => v.Value,
- v => new ProductId(v));
- x.HasKey(e => e.Id);
- x.Property(e => e.Id).UseAutoincrement();
- }),
- target => target.Entity(
+ common => common.Entity(
x =>
{
x.Property(e => e.Id).HasConversion(
@@ -118,6 +109,8 @@ public void No_repeated_alter_column_for_autoincrement_with_converter()
x.HasKey(e => e.Id);
x.Property(e => e.Id).UseAutoincrement();
}),
+ source => { },
+ target => { },
upOps =>
{
// Should have no operations since the models are the same
@@ -125,7 +118,7 @@ public void No_repeated_alter_column_for_autoincrement_with_converter()
});
[ConditionalFact]
- public void No_migration_operations_when_string_api_matches_convention_with_converter()
+ public void Noop_when_changing_to_autoincrement_property_with_converter()
=> Execute(
source => source.Entity(
"Product",
@@ -133,7 +126,6 @@ public void No_migration_operations_when_string_api_matches_convention_with_conv
{
x.Property("Id");
x.HasKey("Id");
- x.Property("Id").HasAnnotation(SqliteAnnotationNames.ValueGenerationStrategy, SqliteValueGenerationStrategy.Autoincrement);
}),
target => target.Entity(
x =>
@@ -142,7 +134,7 @@ public void No_migration_operations_when_string_api_matches_convention_with_conv
v => v.Value,
v => new ProductId(v));
x.HasKey(e => e.Id);
- // No explicit UseAutoincrement() - should be set by convention
+ x.Property(e => e.Id).UseAutoincrement();
}),
upOps =>
{
From e533351cb27d0100e2c1c5c2c1c97b28e02ec596 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 6 Sep 2025 02:25:48 +0000
Subject: [PATCH 15/39] Fix TryGetAndRemove method in
SqliteAnnotationCodeGenerator
Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
---
.../Internal/SqliteAnnotationCodeGenerator.cs | 13 ++++---------
1 file changed, 4 insertions(+), 9 deletions(-)
diff --git a/src/EFCore.Sqlite.Core/Design/Internal/SqliteAnnotationCodeGenerator.cs b/src/EFCore.Sqlite.Core/Design/Internal/SqliteAnnotationCodeGenerator.cs
index 7ddd3295de5..af3fca781d7 100644
--- a/src/EFCore.Sqlite.Core/Design/Internal/SqliteAnnotationCodeGenerator.cs
+++ b/src/EFCore.Sqlite.Core/Design/Internal/SqliteAnnotationCodeGenerator.cs
@@ -49,7 +49,7 @@ public override IReadOnlyList GenerateFluentApiCalls(
{
var fragments = new List(base.GenerateFluentApiCalls(property, annotations));
- if (TryGetAndRemove(annotations, SqliteAnnotationNames.ValueGenerationStrategy, out var strategy)
+ if (GetAndRemove(annotations, SqliteAnnotationNames.ValueGenerationStrategy) is { } strategy
&& strategy == SqliteValueGenerationStrategy.Autoincrement)
{
var methodInfo = property.DeclaringType is IComplexType
@@ -77,20 +77,15 @@ protected override bool IsHandledByConvention(IProperty property, IAnnotation an
return base.IsHandledByConvention(property, annotation);
}
- private static bool TryGetAndRemove(
- IDictionary annotations,
- string annotationName,
- [NotNullWhen(true)] out T? annotationValue)
+ private static T? GetAndRemove(IDictionary annotations, string annotationName)
{
if (annotations.TryGetValue(annotationName, out var annotation)
&& annotation.Value != null)
{
annotations.Remove(annotationName);
- annotationValue = (T)annotation.Value;
- return true;
+ return (T)annotation.Value;
}
- annotationValue = default;
- return false;
+ return default;
}
}
\ No newline at end of file
From 3769a12bd27102b285a9f85175e79640111da21b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 6 Sep 2025 02:37:24 +0000
Subject: [PATCH 16/39] Address review feedback: remove empty lines and add
convention-based autoincrement test
Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
---
.../Migrations/MigrationsSqliteTest.cs | 34 ++++++++++++++++++-
1 file changed, 33 insertions(+), 1 deletion(-)
diff --git a/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsSqliteTest.cs
index dbc575fae9b..00635868d51 100644
--- a/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsSqliteTest.cs
+++ b/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsSqliteTest.cs
@@ -2348,7 +2348,6 @@ await Test(
""");
}
-
[ConditionalFact]
public virtual async Task Create_table_with_autoincrement_and_value_converter()
{
@@ -2382,6 +2381,39 @@ await Test(
""");
}
+ [ConditionalFact]
+ public virtual async Task Create_table_with_autoincrement_and_value_converter_by_convention()
+ {
+ await Test(
+ builder => { },
+ builder => builder.Entity(
+ x =>
+ {
+ x.Property(e => e.Id).HasConversion(
+ v => v.Value,
+ v => new ProductId(v)); // No explicit UseAutoincrement() call - relies on convention
+ x.HasKey(e => e.Id);
+ x.Property(e => e.Name);
+ }),
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ Assert.Equal("ProductWithStrongId", table.Name);
+ Assert.Equal(2, table.Columns.Count());
+
+ var idColumn = Assert.Single(table.Columns, c => c.Name == "Id");
+ Assert.False(idColumn.IsNullable);
+ });
+
+ AssertSql(
+ """
+CREATE TABLE "ProductWithStrongId" (
+ "Id" INTEGER NOT NULL CONSTRAINT "PK_ProductWithStrongId" PRIMARY KEY AUTOINCREMENT,
+ "Name" TEXT NULL
+);
+""");
+ }
+
[ConditionalFact]
public virtual async Task Create_table_with_composite_primary_key_and_autoincrement_fails()
{
From 28412a4bd358a931370c6aba323ceec5ad528e27 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 6 Sep 2025 02:50:38 +0000
Subject: [PATCH 17/39] Address review feedback: rewrite PR description,
confirm issue fixes, and update test assertions to use Assert.Equal
Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
---
...arpMigrationsGeneratorModelSnapshotTest.cs | 113 +++++++++++++++++-
1 file changed, 108 insertions(+), 5 deletions(-)
diff --git a/test/EFCore.Sqlite.Tests/Design/SqliteCSharpMigrationsGeneratorModelSnapshotTest.cs b/test/EFCore.Sqlite.Tests/Design/SqliteCSharpMigrationsGeneratorModelSnapshotTest.cs
index 1d1a00b0f74..437a35d32d7 100644
--- a/test/EFCore.Sqlite.Tests/Design/SqliteCSharpMigrationsGeneratorModelSnapshotTest.cs
+++ b/test/EFCore.Sqlite.Tests/Design/SqliteCSharpMigrationsGeneratorModelSnapshotTest.cs
@@ -26,7 +26,42 @@ public void Autoincrement_annotation_is_replaced_by_extension_method_call_in_sna
e.Property(p => p.Id).UseAutoincrement();
});
},
- "SqlitePropertyBuilderExtensions.UseAutoincrement(b.Property(\"Id\"));",
+ """
+ //
+ using System;
+ using Microsoft.EntityFrameworkCore;
+ using Microsoft.EntityFrameworkCore.Infrastructure;
+ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+ #nullable disable
+
+ namespace RootNamespace
+ {
+ [DbContext(typeof(DbContext))]
+ partial class Snapshot : ModelSnapshot
+ {
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {
+ #pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "10.0.0");
+
+ modelBuilder.Entity("Microsoft.EntityFrameworkCore.Sqlite.Design.CSharpMigrationsGeneratorSqliteTest+EntityWithAutoincrement", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ SqlitePropertyBuilderExtensions.UseAutoincrement(b.Property("Id"));
+
+ b.HasKey("Id");
+
+ b.ToTable("EntityWithAutoincrement");
+ });
+ #pragma warning restore 612, 618
+ }
+ }
+ }
+ """,
model =>
{
var entity = model.FindEntityType(typeof(EntityWithAutoincrement));
@@ -48,7 +83,43 @@ public void Autoincrement_works_with_value_converter_to_int()
.UseAutoincrement();
});
},
- "SqlitePropertyBuilderExtensions.UseAutoincrement(b.Property(\"Id\"));",
+ """
+ //
+ using System;
+ using Microsoft.EntityFrameworkCore;
+ using Microsoft.EntityFrameworkCore.Infrastructure;
+ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+ #nullable disable
+
+ namespace RootNamespace
+ {
+ [DbContext(typeof(DbContext))]
+ partial class Snapshot : ModelSnapshot
+ {
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {
+ #pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "10.0.0");
+
+ modelBuilder.Entity("Microsoft.EntityFrameworkCore.Sqlite.Design.CSharpMigrationsGeneratorSqliteTest+EntityWithConverterPk", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasConversion();
+
+ SqlitePropertyBuilderExtensions.UseAutoincrement(b.Property("Id"));
+
+ b.HasKey("Id");
+
+ b.ToTable("EntityWithConverterPk");
+ });
+ #pragma warning restore 612, 618
+ }
+ }
+ }
+ """,
model =>
{
var entity = model.FindEntityType(typeof(EntityWithConverterPk));
@@ -69,7 +140,39 @@ public void No_autoincrement_method_call_when_strategy_is_none()
e.Property(p => p.Id).ValueGeneratedNever();
});
},
- "b.Property(\"Id\")",
+ """
+ //
+ using System;
+ using Microsoft.EntityFrameworkCore;
+ using Microsoft.EntityFrameworkCore.Infrastructure;
+ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+ #nullable disable
+
+ namespace RootNamespace
+ {
+ [DbContext(typeof(DbContext))]
+ partial class Snapshot : ModelSnapshot
+ {
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {
+ #pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "10.0.0");
+
+ modelBuilder.Entity("Microsoft.EntityFrameworkCore.Sqlite.Design.CSharpMigrationsGeneratorSqliteTest+EntityWithAutoincrement", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.ToTable("EntityWithAutoincrement");
+ });
+ #pragma warning restore 612, 618
+ }
+ }
+ }
+ """,
model =>
{
var entity = model.FindEntityType(typeof(EntityWithAutoincrement));
@@ -93,7 +196,7 @@ private class EntityWithStringKey
public string Id { get; set; } = null!;
}
- protected void Test(Action buildModel, string expectedCodeFragment, Action assert)
+ protected void Test(Action buildModel, string expected, Action assert)
{
var modelBuilder = CreateConventionalModelBuilder();
modelBuilder.Model.RemoveAnnotation(CoreAnnotationNames.ProductVersion);
@@ -106,7 +209,7 @@ protected void Test(Action buildModel, string expectedCodeFragment
assert(model);
- Assert.Contains(expectedCodeFragment, code);
+ Assert.Equal(expected, code);
}
protected SqliteTestHelpers.TestModelBuilder CreateConventionalModelBuilder()
From 2b52a6bf7cf27beb99bad6846bbaf6b9d8339f84 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 6 Sep 2025 03:47:25 +0000
Subject: [PATCH 18/39] Fix TryGetAndRemove method in
SqliteAnnotationCodeGenerator
Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
---
.../Internal/SqliteAnnotationCodeGenerator.cs | 13 +++++++++----
1 file changed, 9 insertions(+), 4 deletions(-)
diff --git a/src/EFCore.Sqlite.Core/Design/Internal/SqliteAnnotationCodeGenerator.cs b/src/EFCore.Sqlite.Core/Design/Internal/SqliteAnnotationCodeGenerator.cs
index af3fca781d7..7ddd3295de5 100644
--- a/src/EFCore.Sqlite.Core/Design/Internal/SqliteAnnotationCodeGenerator.cs
+++ b/src/EFCore.Sqlite.Core/Design/Internal/SqliteAnnotationCodeGenerator.cs
@@ -49,7 +49,7 @@ public override IReadOnlyList GenerateFluentApiCalls(
{
var fragments = new List(base.GenerateFluentApiCalls(property, annotations));
- if (GetAndRemove(annotations, SqliteAnnotationNames.ValueGenerationStrategy) is { } strategy
+ if (TryGetAndRemove(annotations, SqliteAnnotationNames.ValueGenerationStrategy, out var strategy)
&& strategy == SqliteValueGenerationStrategy.Autoincrement)
{
var methodInfo = property.DeclaringType is IComplexType
@@ -77,15 +77,20 @@ protected override bool IsHandledByConvention(IProperty property, IAnnotation an
return base.IsHandledByConvention(property, annotation);
}
- private static T? GetAndRemove(IDictionary annotations, string annotationName)
+ private static bool TryGetAndRemove(
+ IDictionary annotations,
+ string annotationName,
+ [NotNullWhen(true)] out T? annotationValue)
{
if (annotations.TryGetValue(annotationName, out var annotation)
&& annotation.Value != null)
{
annotations.Remove(annotationName);
- return (T)annotation.Value;
+ annotationValue = (T)annotation.Value;
+ return true;
}
- return default;
+ annotationValue = default;
+ return false;
}
}
\ No newline at end of file
From 0754086ddbf16ebd9f287dc460b0325b14766951 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 6 Sep 2025 04:28:43 +0000
Subject: [PATCH 19/39] Address PR review feedback: remove comments, empty
lines, move and rename test file, fix empty lambda suggestions
Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
---
.../CSharpMigrationsGeneratorSqliteTest.cs} | 200 +++++++++---------
.../Migrations/MigrationsSqliteTest.cs | 4 +-
.../SqliteMetadataExtensionsTest.cs | 2 -
.../Migrations/SqliteModelDifferTest.cs | 8 +-
4 files changed, 109 insertions(+), 105 deletions(-)
rename test/{EFCore.Sqlite.Tests/Design/SqliteCSharpMigrationsGeneratorModelSnapshotTest.cs => EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorSqliteTest.cs} (63%)
diff --git a/test/EFCore.Sqlite.Tests/Design/SqliteCSharpMigrationsGeneratorModelSnapshotTest.cs b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorSqliteTest.cs
similarity index 63%
rename from test/EFCore.Sqlite.Tests/Design/SqliteCSharpMigrationsGeneratorModelSnapshotTest.cs
rename to test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorSqliteTest.cs
index 437a35d32d7..6dedc952b65 100644
--- a/test/EFCore.Sqlite.Tests/Design/SqliteCSharpMigrationsGeneratorModelSnapshotTest.cs
+++ b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorSqliteTest.cs
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
using Microsoft.EntityFrameworkCore.Design.Internal;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
@@ -9,12 +11,41 @@
using Microsoft.EntityFrameworkCore.Sqlite.Design.Internal;
using Microsoft.EntityFrameworkCore.Sqlite.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Sqlite.Storage.Internal;
+using Microsoft.EntityFrameworkCore.TestUtilities;
using Xunit.Sdk;
-namespace Microsoft.EntityFrameworkCore.Sqlite.Design;
+namespace Microsoft.EntityFrameworkCore.Migrations.Design;
public class CSharpMigrationsGeneratorSqliteTest
{
+ protected virtual string AddBoilerPlate(string code, bool usingSystem = false)
+ => $$"""
+//
+{{(usingSystem
+ ? @"using System;
+"
+ : "")}}using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace RootNamespace
+{
+ [DbContext(typeof(DbContext))]
+ partial class Snapshot : ModelSnapshot
+ {
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+{{code}}
+#pragma warning restore 612, 618
+ }
+ }
+}
+
+""";
+
[ConditionalFact]
public void Autoincrement_annotation_is_replaced_by_extension_method_call_in_snapshot()
{
@@ -26,26 +57,11 @@ public void Autoincrement_annotation_is_replaced_by_extension_method_call_in_sna
e.Property(p => p.Id).UseAutoincrement();
});
},
- """
- //
- using System;
- using Microsoft.EntityFrameworkCore;
- using Microsoft.EntityFrameworkCore.Infrastructure;
- using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-
- #nullable disable
-
- namespace RootNamespace
- {
- [DbContext(typeof(DbContext))]
- partial class Snapshot : ModelSnapshot
- {
- protected override void BuildModel(ModelBuilder modelBuilder)
- {
- #pragma warning disable 612, 618
+ AddBoilerPlate(
+ """
modelBuilder.HasAnnotation("ProductVersion", "10.0.0");
- modelBuilder.Entity("Microsoft.EntityFrameworkCore.Sqlite.Design.CSharpMigrationsGeneratorSqliteTest+EntityWithAutoincrement", b =>
+ modelBuilder.Entity("Microsoft.EntityFrameworkCore.Migrations.Design.CSharpMigrationsGeneratorSqliteTest+EntityWithAutoincrement", b =>
{
b.Property("Id")
.ValueGeneratedOnAdd()
@@ -57,11 +73,7 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.ToTable("EntityWithAutoincrement");
});
- #pragma warning restore 612, 618
- }
- }
- }
- """,
+ """),
model =>
{
var entity = model.FindEntityType(typeof(EntityWithAutoincrement));
@@ -78,59 +90,37 @@ public void Autoincrement_works_with_value_converter_to_int()
{
builder.Entity(e =>
{
- e.Property(p => p.Id)
- .HasConversion()
- .UseAutoincrement();
+ e.Property(p => p.Id).HasConversion().UseAutoincrement();
});
},
- """
- //
- using System;
- using Microsoft.EntityFrameworkCore;
- using Microsoft.EntityFrameworkCore.Infrastructure;
- using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-
- #nullable disable
-
- namespace RootNamespace
- {
- [DbContext(typeof(DbContext))]
- partial class Snapshot : ModelSnapshot
- {
- protected override void BuildModel(ModelBuilder modelBuilder)
- {
- #pragma warning disable 612, 618
+ AddBoilerPlate(
+ """
modelBuilder.HasAnnotation("ProductVersion", "10.0.0");
- modelBuilder.Entity("Microsoft.EntityFrameworkCore.Sqlite.Design.CSharpMigrationsGeneratorSqliteTest+EntityWithConverterPk", b =>
+ modelBuilder.Entity("Microsoft.EntityFrameworkCore.Migrations.Design.CSharpMigrationsGeneratorSqliteTest+EntityWithConverterPk", b =>
{
b.Property("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasConversion();
- SqlitePropertyBuilderExtensions.UseAutoincrement(b.Property("Id"));
+ SqlitePropertyBuilderExtensions.UseAutoincrement(b.Property("Id"));
b.HasKey("Id");
b.ToTable("EntityWithConverterPk");
});
- #pragma warning restore 612, 618
- }
- }
- }
- """,
+ """),
model =>
{
var entity = model.FindEntityType(typeof(EntityWithConverterPk));
var property = entity!.FindProperty("Id");
- // Should have autoincrement strategy even with value converter
Assert.Equal(SqliteValueGenerationStrategy.Autoincrement, property!.GetValueGenerationStrategy());
});
}
[ConditionalFact]
- public void No_autoincrement_method_call_when_strategy_is_none()
+ public void No_autoincrement_annotation_generated_for_non_autoincrement_property()
{
Test(
builder =>
@@ -140,39 +130,21 @@ public void No_autoincrement_method_call_when_strategy_is_none()
e.Property(p => p.Id).ValueGeneratedNever();
});
},
- """
- //
- using System;
- using Microsoft.EntityFrameworkCore;
- using Microsoft.EntityFrameworkCore.Infrastructure;
- using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-
- #nullable disable
-
- namespace RootNamespace
- {
- [DbContext(typeof(DbContext))]
- partial class Snapshot : ModelSnapshot
- {
- protected override void BuildModel(ModelBuilder modelBuilder)
- {
- #pragma warning disable 612, 618
+ AddBoilerPlate(
+ """
modelBuilder.HasAnnotation("ProductVersion", "10.0.0");
- modelBuilder.Entity("Microsoft.EntityFrameworkCore.Sqlite.Design.CSharpMigrationsGeneratorSqliteTest+EntityWithAutoincrement", b =>
+ modelBuilder.Entity("Microsoft.EntityFrameworkCore.Migrations.Design.CSharpMigrationsGeneratorSqliteTest+EntityWithAutoincrement", b =>
{
b.Property("Id")
+ .ValueGeneratedNever()
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("EntityWithAutoincrement");
});
- #pragma warning restore 612, 618
- }
- }
- }
- """,
+ """),
model =>
{
var entity = model.FindEntityType(typeof(EntityWithAutoincrement));
@@ -181,35 +153,36 @@ protected override void BuildModel(ModelBuilder modelBuilder)
});
}
- private class EntityWithAutoincrement
- {
- public int Id { get; set; }
- }
-
- private class EntityWithConverterPk
- {
- public long Id { get; set; }
- }
-
- private class EntityWithStringKey
- {
- public string Id { get; set; } = null!;
- }
+ protected void Test(Action buildModel, string expectedCode, Action assert)
+ => Test(buildModel, expectedCode, (m, _) => assert(m));
- protected void Test(Action buildModel, string expected, Action assert)
+ protected void Test(Action buildModel, string expectedCode, Action assert, bool validate = false)
{
var modelBuilder = CreateConventionalModelBuilder();
modelBuilder.Model.RemoveAnnotation(CoreAnnotationNames.ProductVersion);
buildModel(modelBuilder);
- var model = modelBuilder.FinalizeModel(designTime: true);
+ var model = modelBuilder.FinalizeModel(designTime: true, skipValidation: !validate);
+
+ Test(model, expectedCode, assert);
+ }
+ protected void Test(IModel model, string expectedCode, Action assert)
+ {
var generator = CreateMigrationsGenerator();
var code = generator.GenerateSnapshot("RootNamespace", typeof(DbContext), "Snapshot", model);
- assert(model);
-
- Assert.Equal(expected, code);
+ var modelFromSnapshot = BuildModelFromSnapshotSource(code);
+ assert(modelFromSnapshot, model);
+
+ try
+ {
+ Assert.Equal(expectedCode, code, ignoreLineEndingDifferences: true);
+ }
+ catch (EqualException e)
+ {
+ throw new Exception(e.Message + Environment.NewLine + Environment.NewLine + "-- Actual code:" + Environment.NewLine + code);
+ }
}
protected SqliteTestHelpers.TestModelBuilder CreateConventionalModelBuilder()
@@ -219,7 +192,8 @@ protected CSharpMigrationsGenerator CreateMigrationsGenerator()
{
var sqliteTypeMappingSource = new SqliteTypeMappingSource(
TestServiceFactory.Instance.Create(),
- TestServiceFactory.Instance.Create());
+ TestServiceFactory.Instance.Create(),
+ new SqliteSingletonOptions());
var codeHelper = new CSharpHelper(sqliteTypeMappingSource);
@@ -241,4 +215,38 @@ protected CSharpMigrationsGenerator CreateMigrationsGenerator()
return generator;
}
+
+ protected IModel BuildModelFromSnapshotSource(string code)
+ {
+ var compilation = CSharpCompilation.Create(
+ nameof(BuildModelFromSnapshotSource),
+ [CSharpSyntaxTree.ParseText(code)],
+ BuildReferences(),
+ new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
+
+ var assembly = compilation.EmitToImageReference();
+
+ var snapshotType = assembly.GetType("RootNamespace.Snapshot");
+ var snapshot = (ModelSnapshot)Activator.CreateInstance(snapshotType)!;
+
+ var modelBuilder = CreateConventionalModelBuilder();
+ snapshot.BuildModel(modelBuilder);
+ return modelBuilder.FinalizeModel(designTime: true);
+ }
+
+ protected IEnumerable BuildReferences()
+ => [.. TestHelpers.GetReferenceAssemblies(), .. TestHelpers.GetProviderReferenceAssemblies()];
+
+ protected TestHelpers TestHelpers
+ => SqliteTestHelpers.Instance;
+
+ private class EntityWithAutoincrement
+ {
+ public int Id { get; set; }
+ }
+
+ private class EntityWithConverterPk
+ {
+ public long Id { get; set; }
+ }
}
\ No newline at end of file
diff --git a/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsSqliteTest.cs
index 00635868d51..07af5403d8a 100644
--- a/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsSqliteTest.cs
+++ b/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsSqliteTest.cs
@@ -2391,7 +2391,7 @@ await Test(
{
x.Property(e => e.Id).HasConversion(
v => v.Value,
- v => new ProductId(v)); // No explicit UseAutoincrement() call - relies on convention
+ v => new ProductId(v));
x.HasKey(e => e.Id);
x.Property(e => e.Name);
}),
@@ -2432,8 +2432,6 @@ await AssertNotSupportedAsync(
"SQLite AUTOINCREMENT can only be used with a single primary key column.");
}
-
-
[ConditionalFact]
public virtual async Task Alter_column_remove_autoincrement()
{
diff --git a/test/EFCore.Sqlite.Tests/Extensions/SqliteMetadataExtensionsTest.cs b/test/EFCore.Sqlite.Tests/Extensions/SqliteMetadataExtensionsTest.cs
index 55cfb9f0da0..a0e6080ded1 100644
--- a/test/EFCore.Sqlite.Tests/Extensions/SqliteMetadataExtensionsTest.cs
+++ b/test/EFCore.Sqlite.Tests/Extensions/SqliteMetadataExtensionsTest.cs
@@ -92,8 +92,6 @@ public void Generic_UseAutoincrement_sets_value_generation_strategy()
Assert.Equal(SqliteValueGenerationStrategy.Autoincrement, propertyBuilder.Metadata.GetValueGenerationStrategy());
}
-
-
[ConditionalFact]
public void Default_value_generation_strategy_for_integer_primary_key()
{
diff --git a/test/EFCore.Sqlite.Tests/Migrations/SqliteModelDifferTest.cs b/test/EFCore.Sqlite.Tests/Migrations/SqliteModelDifferTest.cs
index ca54b9a2ec1..a5cbb7e67c2 100644
--- a/test/EFCore.Sqlite.Tests/Migrations/SqliteModelDifferTest.cs
+++ b/test/EFCore.Sqlite.Tests/Migrations/SqliteModelDifferTest.cs
@@ -43,7 +43,7 @@ public void Alter_property_add_autoincrement_strategy()
x.Property("Id");
x.HasKey("Id");
}),
- source => source.Entity("Person").Property("Id"),
+ source => { },
target => target.Entity("Person").Property("Id").UseAutoincrement(),
upOps =>
{
@@ -65,7 +65,7 @@ public void Alter_property_remove_autoincrement_strategy()
x.HasKey("Id");
}),
source => source.Entity("Person").Property