1+ // Licensed to the .NET Foundation under one or more agreements.
2+ // The .NET Foundation licenses this file to you under the MIT license.
3+
4+ using Microsoft . EntityFrameworkCore . Storage . ValueConversion ;
5+
6+ namespace Microsoft . EntityFrameworkCore ;
7+
8+ /// <summary>
9+ /// Test for SQLite AUTOINCREMENT with value converters, specifically for issues #30699 and #29519.
10+ /// </summary>
11+ public class SqliteAutoincrementWithConverterTest : IClassFixture < SqliteAutoincrementWithConverterTest . SqliteAutoincrementWithConverterFixture >
12+ {
13+ private const string DatabaseName = "AutoincrementWithConverter" ;
14+
15+ public SqliteAutoincrementWithConverterTest ( SqliteAutoincrementWithConverterFixture fixture )
16+ {
17+ Fixture = fixture ;
18+ }
19+
20+ protected SqliteAutoincrementWithConverterFixture Fixture { get ; }
21+
22+ [ ConditionalFact ]
23+ public virtual async Task Strongly_typed_id_with_converter_gets_autoincrement ( )
24+ {
25+ await using var context = ( PoolableDbContext ) CreateContext ( ) ;
26+
27+ // Ensure the database is created
28+ await context . Database . EnsureCreatedAsync ( ) ;
29+
30+ // Check that the SQL contains AUTOINCREMENT for the strongly-typed ID
31+ var sql = context . Database . GenerateCreateScript ( ) ;
32+ Assert . Contains ( "\" Id\" INTEGER NOT NULL CONSTRAINT \" PK_Products\" PRIMARY KEY AUTOINCREMENT" , sql ) ;
33+ }
34+
35+ [ ConditionalFact ]
36+ public virtual async Task Insert_with_strongly_typed_id_generates_value ( )
37+ {
38+ await using var context = ( PoolableDbContext ) CreateContext ( ) ;
39+ await context . Database . EnsureCreatedAsync ( ) ;
40+
41+ // Insert a product with strongly-typed ID
42+ var product = new Product { Name = "Test Product" } ;
43+ context . Products . Add ( product ) ;
44+ await context . SaveChangesAsync ( ) ;
45+
46+ // The ID should have been generated
47+ Assert . True ( product . Id . Value > 0 ) ;
48+
49+ // Insert another product
50+ var product2 = new Product { Name = "Test Product 2" } ;
51+ context . Products . Add ( product2 ) ;
52+ await context . SaveChangesAsync ( ) ;
53+
54+ // The second ID should be different
55+ Assert . True ( product2 . Id . Value > product . Id . Value ) ;
56+ }
57+
58+ [ ConditionalFact ]
59+ public virtual async Task Migration_consistency_with_value_converter ( )
60+ {
61+ await using var context = ( PoolableDbContext ) CreateContext ( ) ;
62+
63+ // This test ensures that migrations don't generate repeated AlterColumn operations
64+ // by checking that the model annotation is consistent
65+ var property = context . Model . FindEntityType ( typeof ( Product ) ) ! . FindProperty ( nameof ( Product . Id ) ) ! ;
66+ var strategy = property . GetValueGenerationStrategy ( ) ;
67+
68+ Assert . Equal ( SqliteValueGenerationStrategy . Autoincrement , strategy ) ;
69+ }
70+
71+ [ ConditionalFact ]
72+ public virtual async Task Explicit_autoincrement_configuration_is_honored ( )
73+ {
74+ await using var context = ( PoolableDbContext ) CreateContext ( ) ;
75+
76+ // Check that explicitly configured AUTOINCREMENT is honored despite having a converter
77+ var property = context . Model . FindEntityType ( typeof ( Product ) ) ! . FindProperty ( nameof ( Product . Id ) ) ! ;
78+ var strategy = property . GetValueGenerationStrategy ( ) ;
79+
80+ Assert . Equal ( SqliteValueGenerationStrategy . Autoincrement , strategy ) ;
81+
82+ // Verify in the actual SQL generation
83+ var sql = context . Database . GenerateCreateScript ( ) ;
84+ Assert . Contains ( "AUTOINCREMENT" , sql ) ;
85+ }
86+
87+ protected virtual DbContext CreateContext ( )
88+ => Fixture . CreateContext ( ) ;
89+
90+ public class SqliteAutoincrementWithConverterFixture : SharedStoreFixtureBase < SqliteAutoincrementWithConverterTest . PoolableDbContext >
91+ {
92+ protected override string StoreName
93+ => DatabaseName ;
94+
95+ protected override ITestStoreFactory TestStoreFactory
96+ => SqliteTestStoreFactory . Instance ;
97+
98+ protected override IServiceCollection AddServices ( IServiceCollection serviceCollection )
99+ => base . AddServices ( serviceCollection ) ;
100+
101+ public override DbContextOptionsBuilder AddOptions ( DbContextOptionsBuilder builder )
102+ => base . AddOptions ( builder ) ;
103+
104+ protected override void OnModelCreating ( ModelBuilder modelBuilder , DbContext context )
105+ {
106+ modelBuilder . Entity < Product > ( b =>
107+ {
108+ b . Property ( e => e . Id ) . HasConversion (
109+ v => v . Value ,
110+ v => new ProductId ( v ) ) ;
111+ b . Property ( e => e . Id ) . UseAutoincrement ( ) ; // Explicit configuration
112+ } ) ;
113+
114+ modelBuilder . Entity < Category > ( ) ; // Standard int ID for comparison
115+ }
116+ }
117+
118+ // Test entities
119+ public record struct ProductId ( int Value ) ;
120+
121+ public class Product
122+ {
123+ public ProductId Id { get ; set ; }
124+ public required string Name { get ; set ; }
125+ }
126+
127+ public class Category
128+ {
129+ public int Id { get ; set ; }
130+ public required string Name { get ; set ; }
131+ }
132+
133+ public class PoolableDbContext : DbContext
134+ {
135+ public PoolableDbContext ( DbContextOptions options )
136+ : base ( options )
137+ {
138+ }
139+
140+ public DbSet < Product > Products => Set < Product > ( ) ;
141+ public DbSet < Category > Categories => Set < Category > ( ) ;
142+ }
143+ }
0 commit comments